Estruturando a Suíte E2E com Kotest e Playwright

Depois de concluirmos esta aula, nós:

  • Saberemos configurar os diretórios de código-fonte e recursos dos nossos testes End-to-End.
  • Saberemos iniciar nossa aplicação web antes da execução dos nossos testes End-to-End e pará-la após a conclusão dos testes.
  • Compreenderemos como podemos executar testes End-to-End que utilizam o Kotest Framework e Playwright.

Esta aula assume que:

  • Você está familiarizado com tags e anotações de filtragem no JUnit Platform / Kotest.
  • Você sabe executar testes de integração que utilizam o Kotest Framework.
  • Você está familiarizado com o Kotest Framework.
  • Você sabe criar um build automatizado que executa testes End-to-End isolados dos testes unitários e de integração.

Os Requisitos da Nossa Suíte de Build End-to-End

Os requisitos do nosso build para testes End-to-End são:

  • Nossos testes End-to-End devem possuir um diretório de código-fonte separado. O diretório src/end-to-end-test/kotlin (ou src/e2eTest/kotlin) deve conter o código-fonte das nossas especificações End-to-End.
  • Nossos testes End-to-End devem possuir um diretório de recursos separado. O diretório src/end-to-end-test/resources (ou src/e2eTest/resources) deve conter os recursos dos nossos testes End-to-End (como scripts SQL e propriedades de ambiente).
  • Apenas os testes unitários devem ser executados por padrão no ciclo rápido de desenvolvimento.
  • Devemos ser capazes de executar apenas os testes unitários, apenas os testes de integração ou apenas os testes End-to-End.
  • Nosso ambiente de teste deve iniciar nossa aplicação web antes que nossos testes End-to-End sejam executados e pará-la após a execução dos testes End-to-End.

Vamos começar adicionando um novo diretório de código-fonte ao nosso projeto.

Adicionando um Novo Diretório de Código-Fonte ao Projeto

Quando queremos isolar fisicamente as especificações End-to-End do Kotest das especificações unitárias (src/test/kotlin) e de integração (src/integration-test/kotlin), configuramos três diretórios de teste distintos:

  1. src/test/kotlin: Especificações unitárias rápidas (*Spec.kt).
  2. src/integration-test/kotlin: Especificações de integração com Spring e Testcontainers (*Spec.kt).
  3. src/end-to-end-test/kotlin: Especificações End-to-End com Kotest e Playwright (*Spec.kt).

Em projetos Maven (para equipes que mantêm builds híbridos ou multi-módulos Maven com Kotlin) ou no Gradle JVM Test Suite Plugin, garantimos que o compilador Kotlin (kotlin-maven-plugin ou jvm-test-suite) reconheça o diretório src/end-to-end-test/kotlin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>3.6.0</version>
    <executions>
        <execution>
            <id>add-test-sources</id>
            <phase>generate-test-sources</phase>
            <goals>
                <goal>add-test-source</goal>
            </goals>
            <configuration>
                <sources>
                    <source>src/integration-test/kotlin</source>
                    <source>src/end-to-end-test/kotlin</source>
                </sources>
            </configuration>
        </execution>
    </executions>
</plugin>

A seguir, adicionaremos o diretório de recursos correspondente.

Adicionando um Novo Diretório de Recursos ao Projeto

Da mesma forma que adicionamos o diretório de código-fonte, precisamos registrar o diretório de recursos src/end-to-end-test/resources para que os scripts SQL e configurações dos testes E2E fiquem disponíveis no classpath durante a fase de verificação:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
        <execution>
            <id>add-end-to-end-test-resources</id>
            <phase>generate-test-resources</phase>
            <goals>
                <goal>add-test-resource</goal>
            </goals>
            <configuration>
                <resources>
                    <resource>
                        <filtering>true</filtering>
                        <directory>src/end-to-end-test/resources</directory>
                    </resource>
                </resources>
            </configuration>
        </execution>

Vamos avançar e descobrir como configuramos os perfis de execução para isolar cada nível da pirâmide de testes.

Configurando Perfis de Execução e Categorias (@Tag / @EndToEndTest)

Quando executamos tanto testes de integração quanto testes End-to-End no motor do JUnit Platform (com kotest-runner-junit5), utilizamos anotações de marcação para distinguir o tipo de especificação:

1
2
3
4
5
6
7
8
package com.testwithspring.master

import org.junit.jupiter.api.Tag

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationTarget.AnnotationRetention.RUNTIME)
@Tag("endToEndTest")
annotation class EndToEndTest

Para controlar quais especificações são executadas em cada perfil:

Primeiro, definimos uma propriedade skip.failsafe.plugin (por padrão true no perfil dev) para que o build padrão execute apenas os testes unitários rápidos.

Segundo, criamos um perfil dedicado chamado end-to-end-test. Este perfil garante que apenas os testes End-to-End marcados com @EndToEndTest (endToEndTest) sejam executados quando o perfil end-to-end-test estiver ativo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<properties>
    <kotlin.version>2.4.21</kotlin.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <failsafe.junit.category>integrationTest</failsafe.junit.category>
    <skip.unit.tests>false</skip.unit.tests>
    <skip.failsafe.plugin>true</skip.failsafe.plugin>
</properties>

<profiles>
    <profile>
        <id>dev</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    <profile>
        <id>end-to-end-test</id>
        <properties>
            <failsafe.junit.category>endToEndTest</failsafe.junit.category>
            <skip.failsafe.plugin>false</skip.failsafe.plugin>
            <skip.unit.tests>true</skip.unit.tests>
        </properties>
    </profile>
    <profile>
        <id>integration-test</id>
        <properties>
            <failsafe.junit.category>integrationTest</failsafe.junit.category>
            <skip.failsafe.plugin>false</skip.failsafe.plugin>
            <skip.unit.tests>true</skip.unit.tests>
        </properties>
    </profile>
</profiles>

Terceiro, configuramos o plugin de verificação (maven-failsafe-plugin no Maven ou a task correspondente no Gradle) para filtrar pelo grupo ${failsafe.junit.category} e incluir todas as classes *Spec:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>3.5.2</version>
    <executions>
        <execution>
            <id>integration-tests</id>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
            <configuration>
                <groups>${failsafe.junit.category}</groups>
                <includes>
                    <include>**/*Spec.class</include>
                </includes>
                <skipTests>${skip.failsafe.plugin}</skipTests>
            </configuration>
        </execution>
    </executions>
</plugin>

Vamos avançar e descobrir como iniciamos e paramos nossa aplicação web para os testes End-to-End.

Iniciando e Parando Nossa Aplicação Web

Antes de podermos executar nossos testes End-to-End com Playwright, temos que iniciar nossa aplicação web antes que os testes End-to-End rodem e pará-la após a conclusão dos testes. Existem duas abordagens arquiteturais para fazer isso:

  1. Em aplicações Spring Framework tradicionais (sem Spring Boot): Iniciar um container servlet externo (como Jetty ou Tomcat) na fase pre-integration-test e pará-lo na fase post-integration-test apenas dentro do perfil end-to-end-test, para não desacelerar os testes de integração.
  2. Em aplicações Spring Boot 4 modernas (Recomendado): Iniciar e parar o servidor web embarcado diretamente através do suporte de testes do Spring Boot (@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) ou DEFINED_PORT) integrado ao SpringExtension do Kotest e ao Testcontainers! Falaremos em detalhes sobre essa configuração nas próximas aulas deste tópico.

Executando Nossos Testes End-to-End

Quando executamos a suíte End-to-End (por exemplo, mvn clean verify -P end-to-end-test ou ./gradlew endToEndTest), o fluxo completo ocorre em três etapas claras:

  1. O servidor da aplicação web (e o banco de dados PostgreSQL via Testcontainers) é iniciado antes da execução das especificações End-to-End.
  2. O runner do JUnit Platform executa apenas as especificações Kotest que pertencem à categoria endToEndTest (como ShowMessageTextSpec ou TaskEndToEndSpec).
  3. O navegador Playwright e o servidor web são encerrados de forma limpa após a execução de todas as especificações End-to-End.

Vamos resumir o que aprendemos nesta aula.

Resumo

Esta aula nos ensinou três coisas:

  • Devemos manter diretórios de código-fonte (src/end-to-end-test/kotlin) e recursos (src/end-to-end-test/resources) dedicados para as especificações End-to-End.
  • Se estivermos usando o Spring Boot 4, podemos iniciar e parar nossa aplicação web aproveitando seu suporte nativo de testes (@SpringBootTest(webEnvironment = ...)).
  • Podemos isolar a execução de testes unitários, de integração e End-to-End combinando perfis/tasks de build e anotações de categoria (@EndToEndTest).