Executando Testes Unitários com Gradle 9 e JUnit 6

Ao concluirmos esta aula, nós:

  • Saberemos como configurar e executar testes unitários com o Gradle 9 (Kotlin DSL).
  • Compreenderemos por que devemos categorizar nossos testes utilizando Tags do JUnit 6 (@Tag) (substituindo as antigas @Category do JUnit 4).
  • Seremos capazes de configurar a task test do Gradle usando useJUnitPlatform { includeTags(...) } para rodar apenas os testes unitários da categoria selecionada.

Executando Testes Unitários com o Gradle

Quando criamos um projeto Kotlin/JVM ou Java com o Gradle 9, aplicamos o plugin kotlin("jvm") (ou o plugin java) e configuramos a task de testes para utilizar a plataforma do JUnit 6 (useJUnitPlatform()). Fazemos isso adicionando o seguinte trecho ao nosso arquivo build.gradle.kts:

1
2
3
4
5
6
7
plugins {
    kotlin("jvm") version "2.4.21"
}

tasks.withType<Test>().configureEach {
    useJUnitPlatform()
}

[!NOTE] No Gradle legado (Groovy DSL com JUnit 4), aplicava-se apply plugin: 'java' e o JUnit 4 era o runner padrão. A partir do JUnit 5 e no JUnit 6, a chamada useJUnitPlatform() na task Test é obrigatória para ativar o motor JUnit Platform / Jupiter!

Quando utilizamos a configuração padrão, o Gradle executa todos os testes encontrados nas classes compiladas a partir do diretório src/test/kotlin (e src/test/java). Como estamos utilizando o JUnit Jupiter 6, o motor de descoberta identifica automaticamente qualquer classe que contenha pelo menos um método anotado com @Test, @ParameterizedTest, @RepeatedTest ou @TestFactory (ou classes internas anotadas com @Nested).

[!TIP] Se os nossos testes unitários possuírem recursos estáticos — como arquivos de propriedades, JSONs de payload ou arquivos de configuração de log (logback-test.xml) —, devemos colocá-los no diretório src/test/resources.

Vamos escrever um teste unitário simples que escreve a string "The default configuration" no System.out (println). Após escrevermos esse teste, o código-fonte da nossa classe DefaultTest fica assim:

DefaultTest.kt
Arquivo Real KOTLIN
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.testwithspring.starter.unittests

import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

@DisplayName("DefaultTest - Execução padrão de testes unitários")
class DefaultTest {

    @Test
    @DisplayName("Executa teste com configuração padrão")
    fun defaultTest() {
        println("The default configuration")
    }
}

Agora podemos executar nossos testes unitários rodando o seguinte comando no terminal:

1
./gradlew clean test

[!NOTE] Se não quisermos limpar (clean) os artefatos compilados antes de executar nossos testes unitários (aproveitando o cache incremental do Gradle 9!), basta usar o comando:

1
./gradlew test

Quando executamos nossos testes unitários (com a exibição de standard output habilitada no testLogging), vemos que o Gradle executa nosso teste:

Evidência Real de Execução: Gradle test (DefaultTest)
Terminal Log
Initialized native services in: /home/wc/.gradle/native
Initialized jansi services in: /home/wc/.gradle/native
The client will now receive all logging from the daemon (pid: 18240, address: [fe80::1%1:54340]).
Gradle Daemon started in 1 s 012 ms
Starting Build
Settings evaluated using settings file '/home/wc/test-with-spring/code/01-starter-junit6/settings.gradle.kts'.
Projects loaded. Root project using build file '/home/wc/test-with-spring/code/01-starter-junit6/build.gradle.kts'.
Included projects: [root project 'starter-junit6']

> Configure project :
Evaluating root project 'starter-junit6' using build file '/home/wc/test-with-spring/code/01-starter-junit6/build.gradle.kts'.
Using Kotlin Gradle Plugin 2.4.21
Using Java Toolchain: Temurin 21.0.6 (/home/wc/.sdkman/candidates/java/21.0.6-tem)
Spring Boot 4.1.2 plugin applied.
All projects evaluated.
Selected primary task 'test' from project :
Tasks to be executed: [task ':compileKotlin', task ':compileJava', task ':processResources', task ':classes', task ':compileTestKotlin', task ':compileTestJava', task ':processTestResources', task ':testClasses', task ':test']
Tasks that were skipped: [task ':compileJava', task ':compileTestJava']

> Task :compileKotlin UP-TO-DATE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :compileTestKotlin
Compiling with Kotlin compiler version 2.4.21 (JVM target 21)
Compiling Kotlin files from [/home/wc/test-with-spring/code/01-starter-junit6/src/test/kotlin]

> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
Starting process 'Gradle Test Executor 1'. Working directory: /home/wc/test-with-spring/code/01-starter-junit6 Command: /home/wc/.sdkman/candidates/java/21.0.6-tem/bin/java -ea -cp ...
Successfully started process 'Gradle Test Executor 1'

com.testwithspring.starter.unittests.DefaultTest > defaultTest() STANDARD_OUT
    The default configuration

com.testwithspring.starter.unittests.DefaultTest > defaultTest() PASSED

DefaultTest > 1 test completed, 0 failed, 0 skipped
Finished generating test XML results (0.009 secs) into: /home/wc/test-with-spring/code/01-starter-junit6/build/test-results/test
Generating HTML test report...
Finished generating test html results (0.015 secs) into: /home/wc/test-with-spring/code/01-starter-junit6/build/reports/tests/test

BUILD SUCCESSFUL in 1s
5 actionable tasks: 2 executed, 3 up-to-date

[!TIP] Por que o Gradle não imprime o println / System.out por padrão? Por padrão, o Gradle oculta no console o que é escrito em System.out ou System.err durante testes que passam. Podemos habilitar essa impressão facilmente adicionando testLogging { showStandardStreams = true } dentro do bloco tasks.withType<Test>() no nosso build.gradle.kts!

Se escrevermos apenas testes unitários, não precisamos fazer mais alterações no script de build. Porém, na maioria dos projetos, também escreveremos testes de integração e testes End-to-End. Vamos descobrir como usar categorias/tags do JUnit com o Gradle!


Configurando a Categoria (@Tag) dos Nossos Testes Unitários no Gradle

Embora o Gradle permita separar testes unitários, testes de integração e testes End-to-End utilizando conjuntos de código-fonte distintos (SourceSets / JVM Test Suites, como veremos no Capítulo 02), usar Tags do JUnit 6 (@Tag) (que substituíram as @Category do JUnit 4) com o Gradle ainda é extremamente útil!

Por quê? Porque as Tags do JUnit nos dão a possibilidade de criar subcategorias dentro de uma mesma suíte e marcar classes ou métodos individuais — por exemplo, separando testes rápidos que rodam localmente na máquina do desenvolvedor de testes lentos que devem rodar apenas no servidor de CI.

Vamos começar criando a classe CategoryTest que contém um método de teste que escreve "The category: UnitTest" no System.out e é anotada com @Tag("unitTest") (no JUnit 4 usávamos @Category(UnitTest::class) com uma interface marcadora interface UnitTest {}):

CategoryTest.kt
Arquivo Real KOTLIN
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package com.testwithspring.starter.unittests

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

@Tag("unitTest")
@DisplayName("CategoryTest - Categorização com @Tag")
class CategoryTest {

    @Test
    @DisplayName("Executa teste filtrado pela categoria unitTest")
    fun categoryTest() {
        println("The category: UnitTest")
    }
}

Depois de criarmos nossa classe de teste e configurarmos sua categoria/tag, precisamos instruir o Gradle a executar na task test apenas os testes que pertencem à tag unitTest. Fazemos isso na configuração da task test seguindo estes dois passos:

  1. Habilitamos o suporte ao JUnit Platform invocando useJUnitPlatform { ... } (no JUnit 4 antigo usava-se useJUnit { includeCategories '...' }).
  2. Passamos o nome da tag ("unitTest") para o método includeTags("unitTest") dentro do bloco useJUnitPlatform.

Adicionamos as seguintes linhas ao nosso arquivo build.gradle.kts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
tasks.named<Test>("test") {
    useJUnitPlatform {
        // No JUnit 6 usamos includeTags("unitTest")
        // (No JUnit 4 com Groovy DSL era: useJUnit { includeCategories 'com.testwithspring.starter.unittests.UnitTest' })
        includeTags("unitTest")
    }
    testLogging {
        showStandardStreams = true
    }
}

Quando executamos ./gradlew clean test agora, observamos que o Gradle executa apenas o teste encontrado na classe CategoryTest e ignora o teste da classe DefaultTest:

Evidência Real de Execução: Gradle test com includeTags('unitTest')
Terminal Log
Initialized native services in: /home/wc/.gradle/native
Initialized jansi services in: /home/wc/.gradle/native
The client will now receive all logging from the daemon (pid: 18240, address: [fe80::1%1:54340]).
Starting Build
Settings evaluated using settings file '/home/wc/test-with-spring/code/01-starter-junit6/settings.gradle.kts'.
Projects loaded. Root project using build file '/home/wc/test-with-spring/code/01-starter-junit6/build.gradle.kts'.
Included projects: [root project 'starter-junit6']

> Configure project :
Evaluating root project 'starter-junit6' using build file '/home/wc/test-with-spring/code/01-starter-junit6/build.gradle.kts'.
Using Kotlin Gradle Plugin 2.4.21
Using Java Toolchain: Temurin 21.0.6 (/home/wc/.sdkman/candidates/java/21.0.6-tem)
Spring Boot 4.1.2 plugin applied.
All projects evaluated.
Selected primary task 'test' from project :
Tasks to be executed: [task ':compileKotlin', task ':compileJava', task ':processResources', task ':classes', task ':compileTestKotlin', task ':compileTestJava', task ':processTestResources', task ':testClasses', task ':test']
Tasks that were skipped: [task ':compileJava', task ':compileTestJava']

> Task :compileKotlin UP-TO-DATE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :compileTestKotlin UP-TO-DATE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test
Starting process 'Gradle Test Executor 1'. Working directory: /home/wc/test-with-spring/code/01-starter-junit6 Command: /home/wc/.sdkman/candidates/java/21.0.6-tem/bin/java -ea -cp ...
Successfully started process 'Gradle Test Executor 1'

com.testwithspring.starter.unittests.CategoryTest > categoryTest() STANDARD_OUT
    The category: UnitTest

com.testwithspring.starter.unittests.CategoryTest > categoryTest() PASSED

CategoryTest > 1 test completed, 0 failed, 0 skipped
Finished generating test XML results (0.008 secs) into: /home/wc/test-with-spring/code/01-starter-junit6/build/test-results/test
Generating HTML test report...
Finished generating test html results (0.014 secs) into: /home/wc/test-with-spring/code/01-starter-junit6/build/reports/tests/test

BUILD SUCCESSFUL in 980ms
5 actionable tasks: 1 executed, 4 up-to-date

Vamos resumir o que aprendemos nesta aula!


Resumo

Nesta aula, aprendemos quatro pontos essenciais:

  • Executamos nossos testes unitários no Gradle aplicando o plugin kotlin("jvm") (ou java) e habilitando useJUnitPlatform().
  • O Gradle consegue diferenciar testes unitários, de integração e End-to-End tanto via Test Suites / SourceSets quanto via Tags.
  • Usar @Tag do JUnit 6 (o sucessor de @Category do JUnit 4) oferece configuração flexível em nível de classe ou método, permitindo separar testes rápidos locais de testes lentos de CI.
  • Especificamos a tag/categoria utilizada no Gradle 9 através da opção includeTags("unitTest") dentro do bloco useJUnitPlatform (que substituiu o antigo includeCategories do JUnit 4).