Configurando Source Sets no Gradle 9 com jvm-test-suite

Ao concluirmos esta aula, nós:

  • Seremos capazes de adicionar suítes de teste (test sets / jvm-test-suite) customizadas ao nosso build no Gradle 9 (Kotlin DSL).
  • Saberemos como declarar as dependências exclusivas dos nossos testes de integração.
  • Compreenderemos como executar apenas testes unitários, apenas testes de integração ou todos os testes com o Gradle.

Os Requisitos do Nosso Build Gradle

Os requisitos do nosso build no Gradle são:

  • Nossos testes de integração devem ter um diretório de código-fonte separado: o diretório src/integration-test/kotlin (ou src/integrationTest/kotlin) deve conter o código-fonte dos nossos testes de integração.
  • Nossos testes de integração devem ter um diretório de recursos separado: o diretório src/integration-test/resources deve conter os recursos dos nossos testes de integração.
  • Devemos ser capazes de executar apenas os testes unitários, apenas os testes de integração ou todos os testes juntos.
  • Se um teste de integração falhar, o nosso build também deve falhar.
  • Os testes unitários e os testes de integração devem gerar relatórios HTML em diretórios separados (para que um relatório não sobrescreva o outro).

Vamos começar adicionando um novo conjunto de testes (test suite) ao nosso build Gradle!


Adicionando Suítes de Teste ao Nosso Build Gradle

No Gradle antigo (Gradle 3.x a 6.x), precisávamos de um plugin externo de terceiros (org.unbroken-dome.test-sets) para criar source sets de teste adicionais sem código boilerplate.

A partir do Gradle 7.3+ e de forma nativa e padronizada no Gradle 9 (Kotlin DSL), o próprio Gradle inclui o plugin oficial jvm-test-suite embutido na distribuição! Ainda assim, vamos compreender como ambos funcionam: a evolução histórica do testSets para a DSL declarativa moderna testing { suites { ... } } do jvm-test-suite.

Passo 1: Aplicando os Plugins no build.gradle.kts

Para usarmos o suporte nativo a múltiplas suítes de teste na JVM com Kotlin e JUnit Jupiter 6, declaramos os plugins no bloco plugins do nosso arquivo build.gradle.kts:

1
2
3
4
5
6
7
8
plugins {
    kotlin("jvm") version "2.4.21"
    `jvm-test-suite`
}

repositories {
    mavenCentral()
}

Passo 2: Configurando os Diretórios de Código-Fonte e Recursos dos Testes de Integração

No plugin clássico org.unbroken-dome.test-sets (em Groovy), definíamos testSets { integrationTest { dirName = 'integration-test' } }.

No Gradle 9 com jvm-test-suite, registramos uma nova suíte chamada integrationTest do tipo JvmTestSuite dentro do bloco testing.suites. Por convenção, o Gradle cria os diretórios src/integrationTest/kotlin e src/integrationTest/resources. Se quisermos usar exatamente os diretórios com hífen (src/integration-test/kotlin e src/integration-test/resources), configuramos o sources da suíte:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            useJUnitJupiter("6.0.3")
        }

        register<JvmTestSuite>("integrationTest") {
            useJUnitJupiter("6.0.3")

            sources {
                kotlin {
                    setSrcDirs(listOf("src/integration-test/kotlin"))
                }
                resources {
                    setSrcDirs(listOf("src/integration-test/resources"))
                }
            }
        }
    }
}

Vamos descobrir agora como declarar as dependências dos nossos testes de integração!


Declarando as Dependências dos Nossos Testes de Integração

Quando adicionamos a suíte integrationTest ao nosso build, o Gradle cria configurações de dependência dedicadas para ela:

  • integrationTestImplementation: Contém as dependências necessárias para compilar e rodar nossos testes de integração (como spring-boot-starter-test, testcontainers, mockk, etc.).
  • integrationTestRuntimeOnly: Contém as dependências necessárias apenas em tempo de execução dos testes de integração (como drivers JDBC ou o junit-platform-launcher).

Além disso, para que os nossos testes de integração possam acessar as classes de produção do projeto (main) e também reutilizar utilitários ou builders de teste unitário, podemos associar o classpath de integrationTest ou declarar as dependências diretamente dentro do bloco dependencies da suíte integrationTest:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
testing {
    suites {
        register<JvmTestSuite>("integrationTest") {
            useJUnitJupiter("6.0.3")

            sources {
                kotlin.setSrcDirs(listOf("src/integration-test/kotlin"))
                resources.setSrcDirs(listOf("src/integration-test/resources"))
            }

            dependencies {
                // Permite que os testes de integração enxerguem o código de produção do projeto
                implementation(project())

                // Adicione aqui as dependências necessárias para compilar e rodar os testes de integração
                implementation("org.springframework.boot:spring-boot-starter-test")
                implementation("org.testcontainers:postgresql:2.0.5")
            }
        }
    }
}

Ou, se preferirmos usar o bloco dependencies { ... } tradicional de nível superior do build.gradle.kts, podemos estender as configurações de testImplementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
val integrationTestImplementation by configurations.getting {
    extendsFrom(configurations.testImplementation.get())
}
val integrationTestRuntimeOnly by configurations.getting {
    extendsFrom(configurations.testRuntimeOnly.get())
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:6.0.3")

    integrationTestImplementation("org.springframework.boot:spring-boot-starter-test")
}

Escrevendo um Teste de Integração Simples

A classe SampleIntegrationTest contém um teste de integração que imprime a string "Tag: integrationTest" na saída padrão (System.out). Para categorizarmos esta classe de teste no JUnit Jupiter 6, nós a anotamos com @Tag("integrationTest") (ou com a nossa meta-anotação @IntegrationTest):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.testwithspring.intermediate.integrationtests

import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test

@Tag("integrationTest")
class SampleIntegrationTest {

    @Test
    fun sampleIntegrationTest() {
        println("Tag: integrationTest")
    }
}

A seguir, precisamos configurar a task integrationTest que executa nossos testes de integração.


Configurando a Task que Executa Nossos Testes de Integração

Quando registramos a suíte integrationTest, o Gradle cria automaticamente uma task do tipo Test chamada integrationTest. Para atendermos a todos os requisitos do nosso build, vamos configurar a ordem de execução, os diretórios de relatórios HTML e a filtragem de tags do JUnit Platform em três passos:

Primeiro, garantimos que os testes unitários rodem antes dos testes de integração e que os testes de integração façam parte do ciclo de verificação da task check (e consequentemente da task build):

  1. Fazemos a task check depender de integrationTest (check { dependsOn(integrationTest) }), de modo que gradle build ou gradle check falhe se qualquer teste de integração falhar.
  2. Configuramos shouldRunAfter(test) (ou mustRunAfter(test)) na task integrationTest. Isso garante que, quando ambos os testes forem solicitados no mesmo comando, os testes unitários rápidos sempre rodem primeiro!

Segundo, garantimos que os relatórios HTML e XML dos testes unitários e dos testes de integração sejam gravados em diretórios separados (build/reports/tests/test e build/reports/tests/integrationTest). No Gradle 9 com jvm-test-suite, o Gradle já separa esses diretórios automaticamente por nome da task, mas podemos deixar explícito em tasks.withType<Test>().

Terceiro, configuramos o JUnit Platform (useJUnitPlatform) em cada task para filtrar por @Tag (unitTest vs integrationTest) e exibir o System.out (showStandardStreams = true).

Após realizarmos todas essas configurações, nosso arquivo build.gradle.kts completo fica assim:

 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
plugins {
    kotlin("jvm") version "2.4.21"
    `jvm-test-suite`
}

repositories {
    mavenCentral()
}

testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            useJUnitJupiter("6.0.3")
            targets {
                all {
                    testTask.configure {
                        useJUnitPlatform {
                            includeTags("unitTest")
                        }
                        testLogging {
                            showStandardStreams = true
                        }
                    }
                }
            }
        }

        val integrationTest by registering(JvmTestSuite::class) {
            useJUnitJupiter("6.0.3")

            sources {
                kotlin.setSrcDirs(listOf("src/integration-test/kotlin"))
                resources.setSrcDirs(listOf("src/integration-test/resources"))
            }

            dependencies {
                implementation(project())
            }

            targets {
                all {
                    testTask.configure {
                        shouldRunAfter(test)
                        useJUnitPlatform {
                            includeTags("integrationTest")
                        }
                        testLogging {
                            showStandardStreams = true
                        }
                    }
                }
            }
        }
    }
}

tasks.named("check") {
    dependsOn(testing.suites.named("integrationTest"))
}

Executando Testes Unitários e de Integração no Gradle

Podemos executar nossos testes no terminal usando uma das três opções a seguir:

Primeiro, se quisermos rodar apenas os testes unitários, temos duas alternativas:

  • Executar diretamente a task test: ./gradlew clean test
  • Executar o build completo excluindo a task de integração com a flag -x: ./gradlew clean build -x integrationTest

Quando executamos ./gradlew clean build -x integrationTest, vemos a seguinte saída:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
$ ./gradlew clean build -x integrationTest
> Task :clean
> Task :compileKotlin
> Task :processResources NO-SOURCE
> Task :classes
> Task :jar
> Task :assemble
> Task :compileTestKotlin
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test

com.testwithspring.intermediate.unittests.SampleUnitTest > sampleUnitTest() STANDARD_OUT
    Tag: unitTest

> Task :check
> Task :build

BUILD SUCCESSFUL in 1s

Segundo, se quisermos rodar apenas os testes de integração, temos duas alternativas:

  • Executar diretamente a task integrationTest: ./gradlew clean integrationTest
  • Executar o build completo excluindo os testes unitários: ./gradlew clean build -x test

Quando executamos ./gradlew clean build -x test, vemos a seguinte saída:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
$ ./gradlew clean build -x test
> Task :clean
> Task :compileKotlin
> Task :processResources NO-SOURCE
> Task :classes
> Task :jar
> Task :assemble
> Task :compileIntegrationTestKotlin
> Task :processIntegrationTestResources NO-SOURCE
> Task :integrationTestClasses
> Task :integrationTest

com.testwithspring.intermediate.integrationtests.SampleIntegrationTest > sampleIntegrationTest() STANDARD_OUT
    Tag: integrationTest

> Task :check
> Task :build

BUILD SUCCESSFUL in 1s

Terceiro, se quisermos rodar todos os testes (unitários seguidos dos de integração):

  • Executar ./gradlew clean test integrationTest
  • Ou simplesmente executar ./gradlew clean build

Quando executamos ./gradlew clean build, vemos que os testes unitários rodam primeiro e os testes de integração rodam logo em seguida:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ ./gradlew clean build
> Task :clean
> Task :compileKotlin
> Task :classes
> Task :compileTestKotlin
> Task :test

com.testwithspring.intermediate.unittests.SampleUnitTest > sampleUnitTest() STANDARD_OUT
    Tag: unitTest

> Task :compileIntegrationTestKotlin
> Task :integrationTest

com.testwithspring.intermediate.integrationtests.SampleIntegrationTest > sampleIntegrationTest() STANDARD_OUT
    Tag: integrationTest

> Task :check
> Task :build

BUILD SUCCESSFUL in 2s

Resumo

Esta aula nos ensinou três coisas fundamentais:

  • Podemos adicionar suítes de teste isoladas (integrationTest) ao nosso build no Gradle 9 usando o plugin oficial embutido jvm-test-suite (substituto moderno do antigo plugin org.unbroken-dome.test-sets).
  • Cada suíte de teste possui seu próprio bloco dependencies e configurações (integrationTestImplementation, integrationTestRuntimeOnly), permitindo isolar bibliotecas pesadas de integração do classpath dos testes unitários.
  • Ao vincularmos a suíte integrationTest à task check com shouldRunAfter(test), garantimos que os testes unitários rápidos sempre rodem antes dos testes de integração e que relatórios HTML sejam gerados em diretórios independentes.