Arquitetura do JUnit Platform, Test Engines e Suítes com Tags
Ao concluirmos esta aula, nós:
- Compreenderemos as responsabilidades de um executor de testes (test runner / test engine) no JUnit.
- Saberemos como o antigo
@RunWithdo JUnit 4 evoluiu para a arquitetura do JUnit Platform e@ExtendWithno JUnit Jupiter 6. - Seremos capazes de categorizar testes com
@Tag(substituindo o antigoCategoriesrunner) e criar suítes de teste seletivas com@Suite,@SelectClassese@IncludeTags.
- Você já está familiarizado com a estrutura básica de classes de teste e ciclo de vida no JUnit 6.
O Que É um Test Runner (e Como Ele Evoluiu para Test Engine)?
A responsabilidade fundamental de um executor de testes (test runner) é instanciar nossas classes de teste, executar os métodos de teste encontrados nessas classes e reportar os resultados obtidos (sucesso, falha ou ignorado).
No JUnit 4 clássico, essa arquitetura funcionava da seguinte maneira:
- Todos os test runners precisavam estender a classe abstrata
org.junit.runner.Runner. - Podíamos escolher um test runner customizado anotando nossa classe de teste com
@RunWith. Se uma classe não tivesse essa anotação, o JUnit 4 a executava usando o runner padrão chamadoBlockJUnit4ClassRunner. - Limitação severa: Uma classe de teste podia usar apenas um único test runner por vez (não era possível combinar, por exemplo, o
SpringJUnit4ClassRunnercom o runnerParameterizedna mesma classe).
No JUnit 6, essa arquitetura monolítica foi completamente redesenhada e dividida em três camadas claras:
- JUnit Platform: Serve como a fundação para lançar frameworks de teste na JVM. Define a
TestEngineAPI. - JUnit Jupiter: É a combinação do novo modelo de programação (anotações
@Test,@BeforeEach,@Tag,@ExtendWith) e do motorjunit-jupiter-engineque executa esses testes. - Fim da limitação de um único Runner: Em vez de
@RunWith, o JUnit Jupiter utiliza@ExtendWith(que aceita múltiplas extensões simultaneamente) e@Suite(através dojunit-platform-suite) para orquestrar suítes de teste.
Agora que conhecemos os fundamentos arquiteturais, vamos ver como invocamos motores e extensões não-padrão e como criamos suítes de teste categorizadas.
Substituindo o Antigo @RunWith: Extensões e Suítes Declarativas
No JUnit 4, quando queríamos usar um runner diferente do padrão (como um CustomRunner ou SpringRunner), precisávamos anotar nossa classe com @RunWith(CustomRunner.class):
|
|
No JUnit Jupiter 6, existem dois mecanismos modernos que substituem o antigo @RunWith dependendo do seu objetivo:
- Para adicionar comportamentos ao redor do teste (como suporte ao Spring ou MockK): utilizamos
@ExtendWith, que permite combinar quantas extensões forem necessárias na mesma classe:
|
|
- Para executar uma classe como uma Suíte de Testes agregadora: utilizamos a anotação
@Suitefornecida pelo móduloorg.junit.platform:junit-platform-suite:
|
|
A seguir, usaremos esse conhecimento para criar uma suíte de testes filtrada por categorias/tags — o equivalente moderno no JUnit 6 ao antigo Categories test runner do JUnit 4.
Criando uma Suíte de Testes com @Tag e @Suite (Substituindo o Categories Runner)
A maneira mais simples de criar uma suíte de testes é agrupar classes com @Suite e @SelectClasses. Essa suíte executará todos os métodos de teste encontrados nas classes selecionadas. Contudo, em projetos reais, frequentemente precisamos incluir e/ou excluir determinados testes da nossa suíte (por exemplo, separar testes rápidos da categoria A dos testes da categoria B).
No JUnit 4, isso exigia criar interfaces marcadoras vazias (interface CategoryA), anotar as classes com @Category(CategoryA.class) e usar @RunWith(Categories.class). No JUnit Jupiter 6, todo esse mecanismo ficou muito mais simples e nativo com a anotação @Tag.
Vamos descobrir passo a passo como criar classes categorizadas e suítes seletivas no JUnit 6 com Kotlin.
Passo 1: Definindo Nossos Identificadores de Categoria (Tags)
No JUnit 4, éramos obrigados a criar interfaces Java vazias apenas para servir de marcador:
|
|
No JUnit Jupiter 6, as categorias são representadas por simples Strings através da anotação @Tag("nomeDaTag"). Para manter a segurança de tipos e evitar erros de digitação em Kotlin, uma excelente prática é definir constantes em um object (ou até mesmo criar suas próprias meta-anotações):
|
|
Depois de definirmos nossos identificadores de categoria, precisamos escrever nossas classes de teste e configurar as categorias (tags) dos nossos métodos de teste.
Passo 2: Escrevendo Nossas Classes de Teste com @Tag
Podemos configurar as categorias dos nossos testes utilizando a anotação @Tag (org.junit.jupiter.api.Tag). Ao usar @Tag, seguimos duas regras simples:
- Se quisermos adicionar todos os métodos de teste de uma classe a uma categoria específica, anotamos a própria classe de teste com
@Tag("..."). - Se quisermos adicionar um método de teste individual a uma categoria específica, anotamos apenas a função de teste com
@Tag("...").
Vamos criar duas classes de teste:
Primeiro, a classe ATest contém um método de teste chamado testA(). Esse método simplesmente imprime a string 'This test belongs to the category A' no console. Todos os métodos desta classe pertencem à categoria categoryA.
O código-fonte da classe ATest é o seguinte:
|
|
Segundo, a classe BTest contém um método de teste chamado testB() que imprime a string 'This test belongs to the category B' no console. Todos os métodos encontrados nesta classe pertencem à categoria categoryB.
O código-fonte da classe BTest é o seguinte:
|
|
Agora que criamos nossas classes de teste categorizadas, vamos descobrir como criar as suítes de teste que filtram e executam esses métodos.
Passo 3: Criando Nossas Suítes de Teste (@Suite)
Para usar suítes declarativas no Gradle 9 (Kotlin DSL), incluímos a dependência org.junit.platform:junit-platform-suite na nossa suíte de testes:
|
|
Podemos criar uma suíte de testes filtrada por tags seguindo três passos:
Primeiro, criamos uma classe em Kotlin e a anotamos com @Suite (que substitui o antigo @RunWith(Categories.class)).
Segundo, configuramos as categorias (tags) incluídas e/ou excluídas seguindo estas regras:
- Podemos incluir categorias anotando a classe da suíte com
@IncludeTags(substituta de@IncludeCategory) e passando os nomes das tags desejadas. - Podemos excluir categorias anotando a classe da suíte com
@ExcludeTags(substituta de@ExcludeCategory) e passando os nomes das tags que devem ser ignoradas.
Terceiro, configuramos as classes de teste que fazem parte do escopo de busca da nossa suíte. Fazemos isso anotando a classe da suíte com @SelectClasses (substituta da antiga @SuiteClasses) e informando as referências das classes (ATest::class, BTest::class).
Vamos inspecionar a implementação completa das classes de teste categorizadas, das suítes de execução seletiva e dos testes de arquitetura da plataforma em nosso projeto:
|
|
@Suite, o Gradle 9 permite filtrar tags diretamente na tarefa de teste do build.gradle.kts usando useJUnitPlatform { includeTags("categoryA") }, sem nem mesmo precisar criar classes de suíte extras!
Agora que criamos nossas duas suítes de teste, vamos ver o que acontece quando as executamos.
Executando Nossas Suítes de Teste
Quando executamos as suítes e classes de teste categorizadas com o Gradle (usando --info), vemos a separação nítida e isolada da execução de cada categoria filtrada pelo Test Engine do JUnit Platform:
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: 15302, address: [fe80::1%1:54325]).
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 :
> 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.lifecycle.ATest > testA() STANDARD_OUT
This test belongs to the category A
com.testwithspring.starter.lifecycle.ATest > testA() PASSED
com.testwithspring.starter.lifecycle.BTest > testB() STANDARD_OUT
This test belongs to the category B
com.testwithspring.starter.lifecycle.BTest > testB() PASSED
com.testwithspring.starter.lifecycle.JUnitPlatformArchitectureTest > verifyCategoryAExecution() STANDARD_OUT
Executing test under categoryA tag
com.testwithspring.starter.lifecycle.JUnitPlatformArchitectureTest > verifyCategoryAExecution() PASSED
com.testwithspring.starter.lifecycle.JUnitPlatformArchitectureTest > verifyCategoryBExecution() STANDARD_OUT
Executing test under categoryB tag
com.testwithspring.starter.lifecycle.JUnitPlatformArchitectureTest > verifyCategoryBExecution() PASSED
JUnit Platform Suite & Tag Filtering > 4 tests completed, 0 failed, 0 skipped
Finished generating test XML results (0.010 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.018 secs) into: /home/wc/test-with-spring/code/01-starter-junit6/build/reports/tests/test
BUILD SUCCESSFUL in 1s
5 actionable tasks: 1 executed, 4 up-to-date
Em outras palavras, mesmo que as suítes apontem para @SelectClasses(ATest::class, BTest::class), filtramos e executamos com sucesso apenas os testes da categoria desejada!
Vamos resumir o que aprendemos nesta aula.
Resumo
Esta aula nos ensinou quatro conceitos essenciais:
- Um motor de execução (Test Runner no JUnit 4 / TestEngine no JUnit 6) é responsável por instanciar as classes de teste, executar seus métodos de teste e reportar os resultados.
- Enquanto o JUnit 4 usava
@RunWithe limitava cada classe a um único runner, o JUnit Jupiter 6 separa a plataforma (JUnit Platform) das extensões (@ExtendWith) e suítes (@Suite), permitindo composição livre. - Substituímos as antigas interfaces marcadoras e
@Categorypela anotação nativa@Tag("nome")do JUnit Jupiter. - A combinação de
@Suite,@SelectClasses,@IncludeTagse@ExcludeTagsnos dá controle total para incluir ou excluir grupos de testes em diferentes suítes de execução.