Isolando Especificações de Integração no Gradle 9

Depois de concluirmos esta aula, nós:

  • Saberemos como adicionar suítes de teste customizadas (integrationTest) ao nosso build Gradle 9.8.2 usando o plugin nativo jvm-test-suite.
  • Saberemos como compilar e executar especificações de integração que utilizam o Kotest Framework 6 com diretórios de relatórios HTML separados.

1. Os Requisitos do Nosso Build Gradle

Os requisitos do nosso build Gradle são:

  • Nossos testes de integração devem ter um diretório de código-fonte separado (src/integrationTest/kotlin — ou src/integration-test/kotlin caso customizado).
  • Nossos testes de integração devem ter um diretório de recursos separado (src/integrationTest/resources).
  • Devemos ser capazes de executar apenas os testes unitários, apenas os testes de integração ou todos os testes.
  • Se um teste de integração falhar, 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 (build/reports/tests/test e build/reports/tests/integrationTest).

Vamos começar adicionando a suíte integrationTest ao nosso build Gradle!


2. Adicionando Suítes de Teste ao Build Gradle (jvm-test-suite)

Nas versões antigas do Gradle (Gradle 4), era necessário adicionar um plugin de terceiros (org.unbroken-dome.test-sets) para criar source sets de teste extras.

No Gradle 9.8.2, o plugin oficial jvm-test-suite já vem embutido na distribuição do Gradle (e é aplicado automaticamente pelo plugin java / kotlin("jvm")). Ele cria automaticamente o source set, as configurações de dependência (integrationTestImplementation), a task integrationTest e um diretório de relatório HTML exclusivo!

Vamos declarar os plugins no nosso build.gradle.kts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
plugins {
    kotlin("jvm") version "2.4.21"
    `jvm-test-suite`
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.slf4j:slf4j-api:2.0.17")
    runtimeOnly("ch.qos.logback:logback-classic:1.5.18")

    testImplementation(platform("io.kotest:kotest-bom:6.2.6"))
    testImplementation("io.kotest:kotest-runner-junit5")
    testImplementation("io.kotest:kotest-assertions-core")
}

3. Configurando os Diretórios e a Task da Suíte integrationTest

Agora configuramos o bloco testing.suites no nosso build.gradle.kts seguindo três passos:

Primeiro, registramos uma nova suíte chamada integrationTest do tipo JvmTestSuite. Por padrão, o jvm-test-suite configura:

  • src/integrationTest/kotlin como diretório de código-fonte.
  • src/integrationTest/resources como diretório de recursos.

[!TIP] Se você estiver migrando um projeto legado que utiliza o nome de diretório com hífen (src/integration-test/kotlin e src/integration-test/resources), você pode customizar os diretórios no bloco sources da suíte:

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

Segundo, garantimos que a suíte integrationTest tenha acesso às classes principais do projeto (implementation(project())) e às dependências do Kotest 6, além de configurar shouldRunAfter(test) para que os testes unitários sempre rodem antes dos testes de integração.

Terceiro, configuramos a task check para depender da suíte integrationTest (tasks.named("check") { dependsOn(testing.suites.named("integrationTest")) }).

Veja como fica o nosso build.gradle.kts completo:

 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
60
61
plugins {
    kotlin("jvm") version "2.4.21"
    `jvm-test-suite`
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.slf4j:slf4j-api:2.0.17")
    runtimeOnly("ch.qos.logback:logback-classic:1.5.18")
}

testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            useJUnitJupiter()
            dependencies {
                implementation(platform("io.kotest:kotest-bom:6.2.6"))
                implementation("io.kotest:kotest-runner-junit5")
                implementation("io.kotest:kotest-assertions-core")
            }
            targets {
                all {
                    testTask.configure {
                        systemProperty("kotest.tags", "UnitTest")
                        testLogging {
                            showStandardStreams = true
                        }
                    }
                }
            }
        }

        register<JvmTestSuite>("integrationTest") {
            useJUnitJupiter()
            dependencies {
                implementation(project())
                implementation(platform("io.kotest:kotest-bom:6.2.6"))
                implementation("io.kotest:kotest-runner-junit5")
                implementation("io.kotest:kotest-assertions-core")
            }
            targets {
                all {
                    testTask.configure {
                        shouldRunAfter(test)
                        systemProperty("kotest.tags", "IntegrationTest")
                        testLogging {
                            showStandardStreams = true
                        }
                    }
                }
            }
        }
    }
}

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

[!NOTE] Diferentemente do antigo plugin test-sets (onde precisávamos configurar manualmente reports.html.destination para evitar que o relatório de integrationTest sobrescrevesse o relatório de test), o jvm-test-suite do Gradle 9 já cria diretórios de relatórios HTML separados automaticamente em build/reports/tests/test e build/reports/tests/integrationTest!


4. Escrevendo uma Especificação Simples de Integração

Vamos criar a classe GetMessageSpec dentro de src/integrationTest/kotlin/com/testwithspring/master:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package com.testwithspring.master

import io.kotest.core.annotation.Tags
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe

@Tags("IntegrationTest")
class GetMessageSpec : ShouldSpec({

    val messageService = MessageService()

    should("Get message") {
        println("Integration test: should return the correct message")
        messageService.getMessage() shouldBe "Hello World!"
    }
})

5. Executando Nossos Testes Unitários e de Integração

Podemos executar nossas suítes usando qualquer uma das três opções abaixo:

Opção 1: Executar apenas os testes unitários

  • Rodando ./gradlew clean test
  • Ou rodando ./gradlew clean build -x integrationTest
1
2
3
4
5
6
7
8
$ ./gradlew clean test

> Task :test

com.testwithspring.master.MessageServiceSpec > Get message STANDARD_OUT
    Unit test: should return the correct message

BUILD SUCCESSFUL in 2s

Opção 2: Executar apenas os testes de integração

  • Rodando ./gradlew clean integrationTest
  • Ou rodando ./gradlew clean build -x test
1
2
3
4
5
6
7
8
$ ./gradlew clean integrationTest

> Task :integrationTest

com.testwithspring.master.GetMessageSpec > Get message STANDARD_OUT
    Integration test: should return the correct message

BUILD SUCCESSFUL in 2s

Opção 3: Executar todos os testes (unitários e de integração)

  • Rodando ./gradlew clean test integrationTest
  • Ou rodando ./gradlew clean build (ou ./gradlew check)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ ./gradlew clean build

> Task :test
com.testwithspring.master.MessageServiceSpec > Get message STANDARD_OUT
    Unit test: should return the correct message

> Task :integrationTest
com.testwithspring.master.GetMessageSpec > Get message STANDARD_OUT
    Integration test: should return the correct message

BUILD SUCCESSFUL in 3s

Resumo

Nesta aula, aprendemos três pontos fundamentais:

  • Adicionamos suítes de teste customizadas no Gradle 9 usando o plugin nativo jvm-test-suite sem depender de plugins externos legados.
  • Cada suíte registrada em testing.suites recebe seus próprios diretórios (src/integrationTest/kotlin e src/integrationTest/resources), sua própria task (integrationTest) e seu próprio diretório de relatórios HTML.
  • Vinculamos integrationTest ao ciclo de vida da task check e configuramos shouldRunAfter(test) para garantir feedback rápido: os testes unitários sempre executam primeiro!