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 criar duas suítes de teste que inspecionam o conjunto formado por ATest e BTest:
Primeiro, a classe ATestSuite especifica uma suíte de testes que inclui apenas os métodos de teste pertencentes à tag "categoryA".
O código-fonte da classe ATestSuite fica assim:
|
|
Segundo, a classe BTestSuite especifica uma suíte de testes que inclui apenas os métodos de teste pertencentes à tag "categoryB".
O código-fonte da classe BTestSuite fica assim:
|
|
@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 a classe ATestSuite, apenas a seguinte linha é escrita no console:
|
|
Por outro lado, quando executamos a classe BTestSuite, vemos que apenas a seguinte linha é escrita no console:
|
|
Em outras palavras, mesmo que ambas 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.