Executando Testes E2E com Gradle 9

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

  • Configurar diretórios de código-fonte e recursos dedicados para os testes End-to-End no Gradle 9.
  • Compreender como declarar dependências exclusivas para a suíte de testes End-to-End (como Playwright ou Selenium).
  • Orquestrar a ordem de execução das tasks (test -> integrationTest -> endToEndTest) no ciclo de vida do check e build.
  • Executar os testes End-to-End isoladamente ou junto com toda a pirâmide de testes no Gradle 9.

Esta lição assume que:


1. Os Requisitos do Nosso Build no Gradle 9

Os requisitos do nosso script build.gradle.kts para os testes End-to-End são:

  • Nossos testes End-to-End devem ter um diretório de código-fonte separado: src/endToEndTest/kotlin (ou src/end-to-end-test/kotlin).
  • Nossos testes End-to-End devem ter um diretório de recursos separado: src/endToEndTest/resources (ou src/end-to-end-test/resources).
  • Devemos ser capazes de executar isoladamente apenas os testes unitários (./gradlew test), apenas os testes de integração (./gradlew integrationTest), apenas os testes End-to-End (./gradlew endToEndTest) ou todos os testes (./gradlew check).
  • Se um teste End-to-End falhar, nosso build também deve falhar.
  • Testes unitários, testes de integração e testes End-to-End devem gerar relatórios HTML e XML em diretórios separados (build/reports/tests/test, build/reports/tests/integrationTest e build/reports/tests/endToEndTest).

Vamos começar configurando os diretórios de código e recursos dos nossos testes End-to-End.


2. Configurando os Diretórios de Código e Recursos dos Testes E2E

No curso original (escrito para Gradle 3), utilizava-se o plugin externo org.unbroken-dome.test-sets com a DSL testSets { endToEndTest { dirName = 'end-to-end-test' } }.

No Gradle 9.8+, utilizamos o plugin oficial nativo jvm-test-suite (testing { suites { ... } }), que cria automaticamente o source set endToEndTest, as configurações de dependência isoladas, a task endToEndTest e o diretório de relatórios próprio!

Adicionamos a suíte endToEndTest ao bloco testing do nosso arquivo build.gradle.kts da seguinte forma:

 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
plugins {
    kotlin("jvm") version "2.4.21"
    kotlin("plugin.spring") version "2.4.21"
    id("org.springframework.boot") version "4.1.2"
    id("io.spring.dependency-management") version "1.1.7"
    `jvm-test-suite`
}

testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            useJUnitJupiter("6.0.3")
        }

        val integrationTest by registering(JvmTestSuite::class) {
            useJUnitJupiter("6.0.3")
            dependencies {
                implementation(project())
            }
        }

        val endToEndTest by registering(JvmTestSuite::class) {
            useJUnitJupiter("6.0.3")
            dependencies {
                implementation(project())
            }
        }
    }
}

Por padrão, o jvm-test-suite configura automaticamente:

  • src/endToEndTest/kotlin para o código-fonte em Kotlin dos testes E2E.
  • src/endToEndTest/resources para os arquivos de recursos (como scripts SQL e properties) dos testes E2E.

Nota: Caso você queira manter o nome de diretório com hífens (src/end-to-end-test/kotlin e src/end-to-end-test/resources) exatamente como nos projetos legados, basta configurar o sourceSets explicitamente:

1
2
3
4
sourceSets.named("endToEndTest") {
    kotlin.setSrcDirs(listOf("src/end-to-end-test/kotlin"))
    resources.setSrcDirs(listOf("src/end-to-end-test/resources"))
}

Vamos avançar e ver como declarar as dependências exclusivas dos nossos testes End-to-End.


3. Declarando as Dependências dos Nossos Testes End-to-End

Quando registramos a suíte endToEndTest, o Gradle cria duas configurações de dependência principais para ela:

  • endToEndTestImplementation (sucessora moderna da antiga endToEndTestCompile): Contém as dependências necessárias para compilar e rodar nossos testes End-to-End (como spring-boot-starter-test, playwright, selenium-java e assertj-core).
  • endToEndTestRuntimeOnly (sucessora moderna da antiga endToEndTestRuntime): Contém as dependências necessárias apenas em tempo de execução dos testes E2E (como drivers JDBC ou motores de log).

Podemos declarar essas dependências diretamente dentro do bloco dependencies da suíte endToEndTest (ou no bloco dependencies global do build.gradle.kts):

1
2
3
4
5
6
dependencies {
    endToEndTestImplementation("org.springframework.boot:spring-boot-starter-test")
    endToEndTestImplementation("com.microsoft.playwright:playwright:1.51.0")
    endToEndTestImplementation("org.assertj:assertj-core:3.27.7")
    endToEndTestRuntimeOnly("org.postgresql:postgresql")
}

Compartilhando Utilitários de Teste com java-test-fixtures:

Se você quiser compartilhar classes de constantes (como Tasks e Users) entre test, integrationTest e endToEndTest sem duplicar código, aplique o plugin `java-test-fixtures` no build.gradle.kts, coloque as classes compartilhadas em src/testFixtures/kotlin e adicione implementation(testFixtures(project())) nas suítes!

A seguir, configuraremos a task endToEndTest que executa nossos testes.


4. Configurando a Task que Executa os Testes End-to-End

Precisamos ajustar a configuração da task endToEndTest em dois passos para cumprir todos os requisitos do nosso build:

Primeiro, precisamos garantir a ordem lógica da pirâmide de testes quando invocamos ./gradlew check ou ./gradlew build:

  1. Os testes End-to-End devem ser executados como parte da task check (e falhar o build caso algum teste E2E falhe).
  2. Os testes de integração (integrationTest) devem ser executados depois dos testes unitários (test) e antes dos testes End-to-End (endToEndTest). Isso garante que falhas rápidas em testes unitários ou de integração sejam reportadas antes de abrirmos um navegador real, e que os testes de integração sempre rodem mesmo se um teste E2E falhar.

Segundo, configuramos as opções do JUnit Platform e de logging na task endToEndTest:

  1. Configurar o JUnit Platform para incluir apenas os testes pertencentes à tag/categoria endToEndTest (includeTags("endToEndTest")).
  2. Configurar testLogging { showStandardStreams = true } caso queiramos visualizar no console as mensagens gravadas em System.out ou System.err (como logs de inicialização do navegador).

Veja como fica essa configuração completa no build.gradle.kts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
tasks.named<Test>("endToEndTest") {
    description = "Executa os testes End-to-End."
    group = "verification"

    shouldRunAfter(tasks.named("test"), tasks.named("integrationTest"))

    useJUnitPlatform {
        includeTags("endToEndTest")
    }

    testLogging {
        showStandardStreams = true
        events("passed", "skipped", "failed")
    }
}

tasks.named<Test>("integrationTest") {
    shouldRunAfter(tasks.named("test"))
}

tasks.named("check") {
    dependsOn(tasks.named("integrationTest"), tasks.named("endToEndTest"))
}

5. Iniciando e Parando a Aplicação Web

Antes de rodarmos os testes End-to-End, a aplicação web precisa estar em execução:

  • Com Spring Boot 4.1 (Abordagem Padrão Moderna): Não precisamos de nenhum plugin adicional no Gradle! Quando anotamos nossas classes de teste E2E com @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) (ou DEFINED_PORT), o próprio Spring Boot Test inicia o servidor web embarcado antes dos testes da suíte e o desliga automaticamente ao final.
  • Com Spring Framework Clássico (WAR sem Spring Boot): No curso original para aplicações WAR legadas sem Spring Boot, utilizava-se o plugin Gretty (org.akhikhl.gretty) configurado com integrationTestTask = "endToEndTest", que executava as tasks :appBeforeIntegrationTest (subindo o Jetty na porta 8080) antes da task :endToEndTest e :appAfterIntegrationTest logo depois.

6. Executando os Testes End-to-End no Terminal

Podemos executar nossos testes End-to-End utilizando uma destas duas opções:

Primeiro, para executar apenas os testes End-to-End (sem rodar os testes unitários ou de integração), executamos o comando:

1
./gradlew clean endToEndTest

Ao executar esse comando, o Gradle compilará o código principal e o source set endToEndTest, executará a task :endToEndTest e gerará o relatório HTML exclusivo em build/reports/tests/endToEndTest/index.html:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
> Task :clean
> Task :compileKotlin
> Task :processResources
> Task :classes
> Task :compileEndToEndTestKotlin
> Task :processEndToEndTestResources NO-SOURCE
> Task :endToEndTestClasses
> Task :endToEndTest

com.testwithspring.intermediate.message.ShowMessagePageTest > deve abrir a pagina inicial() PASSED

BUILD SUCCESSFUL in 6s

Segundo, se quisermos executar todas as camadas de teste (unitários, integração e End-to-End) na ordem exata da pirâmide (:test -> :integrationTest -> :endToEndTest), podemos usar um destes dois comandos:

1
2
3
./gradlew clean test integrationTest endToEndTest
## ou simplesmente:
./gradlew clean build

Vamos resumir o que aprendemos nesta lição.


Resumo

Esta lição nos ensinou quatro pontos essenciais:

  • Podemos adicionar source sets de teste customizados (integrationTest e endToEndTest) ao nosso build Gradle 9 utilizando o plugin nativo jvm-test-suite.
  • O plugin jvm-test-suite cria configurações de dependência dedicadas (endToEndTestImplementation e endToEndTestRuntimeOnly) para isolar bibliotecas de automação de browser (como Playwright e Selenium) do código de produção e dos testes unitários.
  • Em aplicações Spring Boot 4.1, iniciamos e encerramos o servidor web diretamente via @SpringBootTest(webEnvironment = ...), sem depender de plugins WAR externos como o Gretty.
  • Ordenamos a execução com shouldRunAfter e check { dependsOn(...) } para garantir que os testes rápidos (unitários e de integração) sempre rodem antes dos testes End-to-End.