Arquitetura de Execução de Testes E2E no Build

Ao concluir esta lição, você será capaz de:

  • Saber como configurar diretórios de código-fonte e de recursos dedicados para os nossos testes End-to-End (E2E).
  • Compreender como iniciar nossa aplicação web antes da execução dos testes E2E e encerrá-la automaticamente após o término dos testes.
  • Executar seletivamente testes unitários, testes de integração ou testes End-to-End no ciclo de vida do build (com Maven Failsafe/Profiles e o paralelo na stack moderna com Spring Boot 4.1 e JUnit 6).

Esta lição assume que:


1. Os Requisitos do Nosso Build para Testes End-to-End

Os requisitos arquiteturais do nosso pipeline de build para suportar testes End-to-End são:

  • Nossos testes End-to-End devem possuir um diretório de código-fonte separado: src/end-to-end-test/kotlin (ou src/endToEndTest/kotlin).
  • Nossos testes End-to-End devem possuir um diretório de recursos separado: src/end-to-end-test/resources (ou src/endToEndTest/resources).
  • Apenas os testes unitários devem ser executados por padrão no fluxo rápido de desenvolvimento.
  • Devemos ser capazes de executar isoladamente apenas os testes unitários, apenas os testes de integração ou apenas os testes End-to-End.
  • Nosso build (ou o suporte de testes do Spring Boot) deve iniciar nossa aplicação web antes da execução dos testes End-to-End e encerrá-la após a conclusão dos testes.
  • Os nomes das nossas classes de teste End-to-End não precisam começar ou terminar com um sufixo arbitrário obrigatório (usamos categorias/tags semânticas como @Tag("endToEndTest") / @EndToEndTest).

Vamos começar adicionando os novos diretórios de código-fonte e recursos ao nosso build.


2. Adicionando Diretórios de Código e Recursos ao Build

Como vimos na configuração dos testes de integração, em builds Maven adicionamos diretórios de código e recursos adicionais usando o Build Helper Maven Plugin (build-helper-maven-plugin). Como já declaramos esse plugin no nosso pom.xml, podemos registrar os diretórios dos testes End-to-End em dois passos:

Primeiro, criamos uma execução (add-end-to-end-test-sources) na fase generate-test-sources que adiciona o diretório de código-fonte dos nossos testes End-to-End (src/end-to-end-test/kotlin) ao build:

 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>
        <!-- Execuções de integration-test omitidas para clareza -->
        <execution>
            <id>add-end-to-end-test-sources</id>
            <phase>generate-test-sources</phase>
            <goals>
                <goal>add-test-source</goal>
            </goals>
            <configuration>
                <sources>
                    <source>src/end-to-end-test/kotlin</source>
                </sources>
            </configuration>
        </execution>
    </executions>
</plugin>

Segundo, criamos uma execução (add-end-to-end-test-resources) na fase generate-test-resources que adiciona o diretório de recursos dos nossos testes End-to-End (src/end-to-end-test/resources) ao build:

 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
33
34
35
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>3.6.0</version>
    <executions>
        <!-- Execuções de integration-test omitidas para clareza -->
        <execution>
            <id>add-end-to-end-test-sources</id>
            <phase>generate-test-sources</phase>
            <goals>
                <goal>add-test-source</goal>
            </goals>
            <configuration>
                <sources>
                    <source>src/end-to-end-test/kotlin</source>
                </sources>
            </configuration>
        </execution>
        <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>
    </executions>
</plugin>

A seguir, veremos como configurar os perfis de execução do build para diferenciar testes de integração e testes End-to-End.


3. Configurando os Perfis de Execução e Tags do JUnit 6

Como executamos tanto os testes de integração quanto os testes End-to-End através do Maven Failsafe Plugin (nas fases integration-test e verify), precisamos ajustar a configuração dos nossos perfis em três passos:

Primeiro, renomeamos a propriedade skip.integration.tests para skip.failsafe.plugin. Fazemos isso porque o nome antigo não descreve mais com precisão o papel da propriedade, já que o plugin Failsafe agora executa tanto testes de integração quanto testes End-to-End:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<properties>
    <kotlin.version>2.4.21</kotlin.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

    <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>integration-test</id>
        <properties>
            <skip.failsafe.plugin>false</skip.failsafe.plugin>
            <skip.unit.tests>true</skip.unit.tests>
        </properties>
    </profile>
</profiles>

Segundo, criamos um novo perfil chamado end-to-end-test. Esse perfil garante que apenas os testes End-to-End sejam executados quando ativado. Além disso, como o Maven Failsafe executa ambos os tipos de teste, criamos uma propriedade chamada junit.tag (equivalente moderna de junit.category no JUnit Platform 6) que define qual tag/meta-anotação (endToEndTest) será filtrada:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<profiles>
    <profile>
        <id>dev</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    <profile>
        <id>end-to-end-test</id>
        <properties>
            <junit.tag>endToEndTest</junit.tag>
            <skip.failsafe.plugin>false</skip.failsafe.plugin>
            <skip.unit.tests>true</skip.unit.tests>
        </properties>
    </profile>
    <profile>
        <id>integration-test</id>
        <properties>
            <skip.failsafe.plugin>false</skip.failsafe.plugin>
            <skip.unit.tests>true</skip.unit.tests>
        </properties>
    </profile>
</profiles>

Terceiro, adicionamos a propriedade <junit.tag>integrationTest</junit.tag> também ao perfil integration-test. Isso garante que o Maven Failsafe execute exclusivamente as classes anotadas com @IntegrationTest (@Tag("integrationTest")) quando rodarmos os testes de integração:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<profiles>
    <profile>
        <id>dev</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    <profile>
        <id>end-to-end-test</id>
        <properties>
            <junit.tag>endToEndTest</junit.tag>
            <skip.failsafe.plugin>false</skip.failsafe.plugin>
            <skip.unit.tests>true</skip.unit.tests>
        </properties>
    </profile>
    <profile>
        <id>integration-test</id>
        <properties>
            <junit.tag>integrationTest</junit.tag>
            <skip.failsafe.plugin>false</skip.failsafe.plugin>
            <skip.unit.tests>true</skip.unit.tests>
        </properties>
    </profile>
</profiles>

Definindo a Meta-Anotação @EndToEndTest em Kotlin:

No JUnit Jupiter 6, criamos nossa meta-anotação semântica combinando @Tag("endToEndTest"):

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

import org.junit.jupiter.api.Tag

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

4. Configurando o Maven Failsafe Plugin

Ajustamos a configuração do maven-failsafe-plugin para cumprir dois requisitos:

  1. Garantir que o valor da propriedade ${junit.tag} especifique o grupo/tag dos métodos de teste invocados (<groups>${junit.tag}</groups>).
  2. Garantir que o plugin não execute nenhum teste quando ${skip.failsafe.plugin} for true.
 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>
                <includes>
                    <include>**/*.class</include>
                </includes>
                <groups>${junit.tag}</groups>
                <skipTests>${skip.failsafe.plugin}</skipTests>
            </configuration>
        </execution>
    </executions>
</plugin>

A seguir, veremos como iniciar e parar nossa aplicação web antes e depois dos testes End-to-End.


5. Iniciando e Parando a Aplicação Web

Antes de rodarmos nossos testes End-to-End, precisamos que a aplicação web esteja em execução ouvindo requisições HTTP reais em uma porta TCP, e que seja encerrada após o término dos testes.

Abordagem 1: Aplicações Spring Framework Tradicionais (WAR externo via Jetty Plugin)

Em aplicações Spring Framework sem Spring Boot empacotadas como .war, configuramos o jetty-maven-plugin dentro do perfil end-to-end-test em três passos:

  1. Declarar o plugin jetty-maven-plugin na seção <build><plugins> do perfil end-to-end-test, apontando para o arquivo WAR gerado e configurando stopPort e stopKey.
  2. Adicionar uma seção <executions> ao plugin do Jetty.
  3. Criar duas execuções:
    • start-jetty: vinculada à fase pre-integration-test, executando o goal start antes dos testes E2E iniciarem.
    • stop-jetty: vinculada à fase post-integration-test, executando o goal stop mesmo que algum teste E2E falhe (graças à separação entre integration-test e verify do Failsafe).
 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
33
34
35
36
37
38
39
40
41
42
43
<profiles>
    <profile>
        <id>end-to-end-test</id>
        <properties>
            <junit.tag>endToEndTest</junit.tag>
            <skip.failsafe.plugin>false</skip.failsafe.plugin>
            <skip.unit.tests>true</skip.unit.tests>
        </properties>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.eclipse.jetty</groupId>
                    <artifactId>jetty-maven-plugin</artifactId>
                    <configuration>
                        <scanIntervalSeconds>0</scanIntervalSeconds>
                        <stopKey>stop</stopKey>
                        <stopPort>9999</stopPort>
                        <webAppConfig>
                            <war>${project.basedir}/target/running-end-to-end-tests.war</war>
                            <contextPath>/</contextPath>
                        </webAppConfig>
                    </configuration>
                    <executions>
                        <execution>
                            <id>start-jetty</id>
                            <phase>pre-integration-test</phase>
                            <goals>
                                <goal>start</goal>
                            </goals>
                        </execution>
                        <execution>
                            <id>stop-jetty</id>
                            <phase>post-integration-test</phase>
                            <goals>
                                <goal>stop</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

Duas observações importantes:

  1. Declaramos o jetty-maven-plugin apenas dentro do perfil end-to-end-test porque não queremos subir o servidor HTTP completo quando executamos os testes de integração (-P integration-test), já que os testes de integração usam MockMvc e subir o container web desnecessariamente deixaria a suíte mais lenta.
  2. Abordagem 2 — Aplicações Spring Boot 4.1 (Recomendada): Se estivermos usando Spring Boot, não precisamos de plugins externos de container no Maven ou Gradle! O próprio suporte de testes do Spring Boot (@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) ou DEFINED_PORT) inicia o servidor embarcado (Tomcat/Jetty/Netty) automaticamente antes dos testes E2E e o encerra ao final. Veremos isso em detalhes no próximo tópico!

6. Executando Nossos Testes End-to-End

Podemos executar nossos testes End-to-End com o seguinte comando:

1
mvn clean verify -P end-to-end-test

Ao executar esse comando no terminal, veremos na saída do console:

  1. O servidor web é iniciado na fase pre-integration-test (ou pelo @SpringBootTest no início da suíte).
  2. O Maven Failsafe Plugin executa exclusivamente os testes marcados com a tag endToEndTest.
  3. O servidor web é encerrado de forma limpa na fase post-integration-test antes da verificação final da fase verify.

Vamos resumir o que aprendemos nesta lição.


Resumo

Esta lição nos ensinou quatro pontos essenciais:

  • Podemos adicionar diretórios de código e recursos dedicados para testes End-to-End no Maven utilizando o build-helper-maven-plugin (e no Gradle 9 de forma nativa com jvm-test-suite, como veremos na próxima aula).
  • Em aplicações Spring Framework clássicas sem Spring Boot, podemos iniciar e parar o servidor web nas fases pre-integration-test e post-integration-test com o jetty-maven-plugin.
  • Em aplicações Spring Boot 4.1, podemos iniciar e parar o servidor web embarcado diretamente através do suporte de testes do @SpringBootTest.
  • Podemos executar de forma isolada tanto testes de integração quanto testes End-to-End utilizando o Maven Failsafe Plugin combinado com filtros de tags (<groups>${junit.tag}</groups>).