Classes de Teste Limpas com Kotlin e JUnit 6
Ao final desta aula, seremos capazes de:
- Criar classes de teste com Kotlin 2.4.21.
- Adicionar funções de preparação (setup), limpeza (teardown) e teste a uma classe de teste.
- Saber como escrever testes aninhados (nested tests) com Kotlin e JUnit Jupiter 6.0.3.
- Compreender a ordem exata de invocação das funções de setup, teardown e teste em uma hierarquia de contextos.
Vamos começar criando uma classe de teste.
Nota — Esta aula assume que:
- Você sabe executar testes Kotlin com Maven ou Gradle 9.8.2 (Kotlin DSL).
- Você conhece os fundamentos de escrita de classes de teste e testes aninhados na plataforma JUnit Jupiter.
Criando uma Classe de Teste
Quando criamos uma nova classe de teste, precisamos criar uma classe Kotlin “normal”. Após criarmos nossa classe de teste, seu código-fonte fica da seguinte forma:
|
|
Nota — Dois pontos importantes:
- Embora seja possível adicionar múltiplas classes em um único arquivo Kotlin (
.kt), essa não é uma boa ideia em suítes de teste corporativas, pois não queremos construir uma suíte de testes que consista em arquivos longos e confusos.- Ao nomear nossas classes de teste, devemos utilizar o sufixo
'Test'.
Em seguida, adicionaremos funções de preparação (setup) e limpeza (teardown) à nossa classe de teste.
Adicionando Funções de Setup e Teardown a uma Classe de Teste
Podemos adicionar funções de setup e teardown à nossa classe de teste seguindo estes passos:
Primeiro, precisamos escrever uma função de setup que seja executada uma única vez antes que qualquer função de teste seja executada. Podemos escrever essa função de setup seguindo estes passos:
- Adicione um
companion objectà nossa classe de teste. - Adicione uma nova função de setup ao
companion objecte implemente essa função escrevendo uma mensagem noSystem.out(viaprintln). - Anote a função de setup com a anotação
@BeforeAll. - Anote a função de setup com a anotação
@JvmStatic. Essa anotação garante que o compilador do Kotlin 2.4.21 gere um método estático no bytecode da classe envolvente (TestClassExampleTest) do objeto, além de um método de instância no própriocompanion object.
Após adicionarmos essa função de setup à nossa classe de teste, seu código-fonte fica da seguinte forma:
|
|
Nota: Não é uma boa prática escrever classes de teste reais que imprimam informações no
System.out. Utilizamos essa técnica aqui exclusivamente porque queremos demonstrar de forma visual a ordem de invocação das funções de setup, teardown e teste.
Segundo, precisamos escrever uma função de teardown que seja executada uma única vez após todas as funções de teste terem sido executadas. Podemos escrever essa função de teardown seguindo estes passos:
- Adicione uma nova função de teardown ao
companion objecte implemente essa função escrevendo uma mensagem noSystem.out. - Anote a função de teardown com as anotações
@AfterAlle@JvmStatic.
Após adicionarmos essa função de teardown à nossa classe de teste, seu código-fonte fica da seguinte forma:
|
|
Nota — Ciclo de Vida de Instância no JUnit Jupiter 6.0.3:
Precisamos usar métodos estáticos (
companion object+@JvmStatic) porque estamos utilizando a configuração padrão de ciclo de vida do JUnit Jupiter 6.0.3 (TestInstance.Lifecycle.PER_METHOD). Isso significa que o JUnit 6 cria um novo objeto de teste para cada método de teste executado. Se configurarmos o JUnit 6 para criar apenas um objeto de teste por classe de teste (via@TestInstance(TestInstance.Lifecycle.PER_CLASS)ou globalmente no arquivojunit-platform.properties), não precisamos usarcompanion objectnem@JvmStaticao escrever essas funções@BeforeAlle@AfterAll.Leitura Adicional:
Terceiro, precisamos escrever uma função de setup que seja invocada antes da execução de cada função de teste. Podemos escrever essa função de setup seguindo estes passos:
- Adicione uma nova função de setup à nossa classe de teste e implemente essa função escrevendo uma mensagem no
System.out. - Anote a função de setup com a anotação
@BeforeEach.
Após adicionarmos essa função de setup à nossa classe de teste, seu código-fonte fica da seguinte forma:
|
|
Quarto, precisamos escrever uma função de teardown que seja invocada após a execução de cada função de teste. Podemos escrever essa função de teardown seguindo estes passos:
- Adicione uma nova função de teardown à nossa classe de teste e implemente essa função escrevendo uma mensagem no
System.out. - Anote a função de teardown com a anotação
@AfterEach.
Após adicionarmos essa função de teardown à nossa classe de teste, seu código-fonte fica da seguinte forma:
|
|
Agora adicionamos todas as funções de setup e teardown à nossa classe de teste. Vamos avançar e descobrir como podemos escrever testes aninhados com Kotlin 2.4.21 e JUnit Jupiter 6.0.3.
Escrevendo Testes Aninhados (Nested Tests)
Quando adicionamos testes aninhados à nossa classe de teste em Kotlin, precisamos utilizar classes internas (inner class). Diferente do Java (onde uma classe interna sem static já é uma inner class por padrão), em Kotlin uma classe declarada dentro de outra é estática por padrão, exigindo explicitamente o modificador inner para que o JUnit Jupiter consiga instanciá-la vinculada à instância da classe externa.
Vamos adicionar algumas classes internas à nossa classe de teste. A ideia deste exercício é demonstrar a ordem de invocação das funções de setup, teardown e teste.
Podemos adicionar as classes internas necessárias à nossa classe de teste seguindo estes passos:
Primeiro, precisamos adicionar uma nova inner class chamada A à nossa classe de teste e anotar a classe interna com a anotação @Nested. Após criarmos a classe A, devemos adicionar uma função de setup, uma de teardown e uma função de teste à classe interna criada.
|
|
Segundo, precisamos adicionar uma nova inner class chamada WhenX dentro da classe A e anotar a classe interna com a anotação @Nested. Após criarmos a classe WhenX, devemos adicionar uma função de setup, uma de teardown e uma função de teste à classe interna criada.
Após adicionarmos essa classe interna à classe A, o código-fonte da nossa classe de teste fica da seguinte forma:
|
|
Agora adicionamos testes aninhados à nossa classe de teste. Em seguida, descobriremos o que acontece quando executamos nossos testes.
Executando Nossos Testes
Quando executamos nossos testes (por exemplo, via ./gradlew test no Gradle 9.8.2), devemos ver a seguinte saída:
|
|
Em outras palavras, o JUnit Jupiter 6.0.3 invoca as funções de setup e teardown seguindo a hierarquia de contexto das funções de teste invocadas (de fora para dentro no @BeforeEach, e de dentro para fora no @AfterEach). Isso significa que podemos eliminar código duplicado de preparação colocando nosso código exatamente no nível correto da hierarquia.
Escrevemos agora uma classe de teste que contém funções aninhadas de setup, teardown e teste, além de compreendermos a ordem de invocação dessas funções. Vamos resumir o que aprendemos nesta aula.
Resumo
Esta aula nos ensinou três pontos fundamentais:
- Não devemos adicionar múltiplas classes de teste de topo em um único arquivo Kotlin, pois isso cria uma suíte de testes composta por arquivos longos e difíceis de manter.
- Se utilizarmos a configuração padrão de ciclo de vida do JUnit Jupiter 6.0.3 (
PER_METHOD) e precisarmos escrever funções de setup e teardown que são executadas apenas uma vez (@BeforeAlle@AfterAll), devemos adicionar essas funções a umcompanion objecte anotá-las com@JvmStatic. - Quando adicionamos testes aninhados (
@Nested) à nossa classe de teste em Kotlin, devemos utilizar classes internas marcadas com a palavra-chaveinner class.