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 @RunWith do JUnit 4 evoluiu para a arquitetura do JUnit Platform e @ExtendWith no JUnit Jupiter 6.
  • Seremos capazes de categorizar testes com @Tag (substituindo o antigo Categories runner) e criar suítes de teste seletivas com @Suite, @SelectClasses e @IncludeTags.
Esta aula assume que:

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 chamado BlockJUnit4ClassRunner.
  • 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 SpringJUnit4ClassRunner com o runner Parameterized na mesma classe).

No JUnit 6, essa arquitetura monolítica foi completamente redesenhada e dividida em três camadas claras:

flowchart TD subgraph Gradle["Gradle 9 (jvm-test-suite)"] Launcher["JUnit Platform Launcher"] end subgraph Platform["JUnit Platform"] EngineAPI["TestEngine API"] end subgraph Engines["Motores de Execução (Test Engines)"] Jupiter["junit-jupiter-engine (JUnit 6)"] Kotest["kotest-runner-junit5 (Kotest 6)"] Suite["junit-platform-suite-engine (@Suite)"] end Launcher --> EngineAPI EngineAPI --> Jupiter EngineAPI --> Kotest EngineAPI --> Suite
  • JUnit Platform: Serve como a fundação para lançar frameworks de teste na JVM. Define a TestEngine API.
  • JUnit Jupiter: É a combinação do novo modelo de programação (anotações @Test, @BeforeEach, @Tag, @ExtendWith) e do motor junit-jupiter-engine que 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 do junit-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):

1
2
3
// Legado JUnit 4 (NÃO utilizado no JUnit 6):
// @RunWith(CustomRunner::class)
// class FooBarTest

No JUnit Jupiter 6, existem dois mecanismos modernos que substituem o antigo @RunWith dependendo do seu objetivo:

  1. 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:
1
2
3
4
5
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(CustomExtension::class)
class FooBarTest {
}
  1. Para executar uma classe como uma Suíte de Testes agregadora: utilizamos a anotação @Suite fornecida pelo módulo org.junit.platform:junit-platform-suite:
1
2
3
4
5
import org.junit.platform.suite.api.Suite

@Suite
class MinhaSuiteDeTestes {
}

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:

1
2
3
// Como era no JUnit 4 (Interfaces marcadoras):
interface CategoryA
interface CategoryB

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):

1
2
3
4
object TestCategories {
    const val CATEGORY_A = "categoryA"
    const val CATEGORY_B = "categoryB"
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test

@Tag("categoryA")
class ATest {

    @Test
    fun testA() {
        println("This test belongs to the category A")
    }
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test

@Tag("categoryB")
class BTest {

    @Test
    fun testB() {
        println("This test belongs to the category B")
    }
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// build.gradle.kts
testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            useJUnitJupiter("6.0.3")
            dependencies {
                implementation("org.junit.platform:junit-platform-suite:1.11.0")
            }
        }
    }
}

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:

1
2
3
4
5
6
7
8
import org.junit.platform.suite.api.IncludeTags
import org.junit.platform.suite.api.SelectClasses
import org.junit.platform.suite.api.Suite

@Suite
@IncludeTags("categoryA")
@SelectClasses(ATest::class, BTest::class)
class ATestSuite

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:

1
2
3
4
5
6
7
8
import org.junit.platform.suite.api.IncludeTags
import org.junit.platform.suite.api.SelectClasses
import org.junit.platform.suite.api.Suite

@Suite
@IncludeTags("categoryB")
@SelectClasses(ATest::class, BTest::class)
class BTestSuite
Dica Prática com Gradle 9: Além de criar classes anotadas com @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:

1
This test belongs to the category A

Por outro lado, quando executamos a classe BTestSuite, vemos que apenas a seguinte linha é escrita no console:

1
This test belongs to the category B

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 @RunWith e 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 @Category pela anotação nativa @Tag("nome") do JUnit Jupiter.
  • A combinação de @Suite, @SelectClasses, @IncludeTags e @ExcludeTags nos dá controle total para incluir ou excluir grupos de testes em diferentes suítes de execução.