Introdução às Classes de Teste no JUnit 6 e Ciclo de Vida
Ao concluirmos esta aula, nós:
- Saberemos como criar novas classes de teste no JUnit Jupiter 6 com Kotlin.
- Compreenderemos como utilizar métodos de configuração (setup) e limpeza (teardown).
- Seremos capazes de adicionar métodos de teste a uma classe de teste e validar sua ordem exata de execução.
Criando uma Nova Classe de Teste
Podemos criar uma nova classe de teste simplesmente declarando uma classe em Kotlin. Embora não exista uma convenção de nomenclatura obrigatória imposta pelo compilador, a convenção mais adotada pela comunidade e esperada por ferramentas de build (como o Gradle 9) para nomear uma classe de teste unitário é adicionar o sufixo Test ao nome da classe ou unidade testada.
Como nesta primeira aula introdutória ainda não estamos escrevendo testes unitários para uma classe de domínio específica, criaremos uma classe de teste chamada SetUpAndTearDownTest. Seu código-fonte inicial é o seguinte:
|
|
public. No JUnit Jupiter 6, essa exigência foi removida (classes podem ser public, internal ou package-private). Além disso, em Kotlin, a visibilidade padrão já é public, portanto nunca precisamos poluir a declaração da classe com a palavra-chave public.
A seguir, descobriremos como podemos adicionar métodos de teste à nossa classe de teste.
Adicionando Métodos de Teste a uma Classe de Teste
Podemos adicionar um novo método de teste a uma classe de teste seguindo dois passos simples:
- Adicionar uma função (
fun) à classe de teste e garantir que a função não retorne nenhum valor (ou seja, que seu tipo de retorno sejaUnit, o padrão implícito do Kotlin). - Anotar a função com a anotação
@Testdo pacoteorg.junit.jupiter.api.Test. Essa anotação informa ao JUnit Jupiter que a função em questão pode e deve ser executada como um caso de teste.
@Test não retornem valores (Unit / void) e não sejam privados (private). Em Kotlin, podemos inclusive usar nomes descritivos entre crases (backticks), como fun `meu teste descritivo`(), ou nomes tradicionais em camelCase.
Vamos adicionar dois métodos de teste à nossa classe de teste:
- O método
testOne()simplesmente imprime a String'Test One'na saída padrão (println). - O método
testTwo()simplesmente imprime a String'Test Two'na saída padrão (println).
Depois de adicionarmos esses dois métodos de teste à nossa classe, seu código-fonte ficará assim:
|
|
Antes de continuarmos, quero alertá-lo sobre três problemas presentes nesta classe de demonstração:
Primeiro, os nomes dos métodos de teste (
testOne e testTwo) não descrevem o resultado esperado nem o comportamento testado. Isso significa que, se um método de teste falhar em um projeto real, teremos que abrir e ler o código-fonte do método antes de descobrir qual é o problema. Isso é um enorme desperdício de tempo.Segundo, nossos métodos de teste não possuem nenhuma asserção (assertion). Em outras palavras, esses métodos de teste nunca podem falhar (a menos que lancem uma exceção inesperada).
Terceiro, nossos métodos de teste escrevem informações no console (
println / System.out). Não há problema em usar println temporariamente para fins de depuração local (embora o uso de um debugger seja preferível), mas jamais devemos commitar testes que escrevem no console em nosso sistema de controle de versão (Git), pois esses testes poluem a saída da suíte de testes e dificultam a leitura dos logs no CI/CD.Utilizo essa abordagem aqui exclusivamente porque quero demonstrar de forma visual quando e em que ordem cada método do ciclo de vida é executado pelo JUnit, e imprimir linhas no console é a maneira mais simples de observar isso.
Acabamos de escrever nossos primeiros métodos de teste. Este é um bom começo. No entanto, quando escrevemos testes unitários para uma aplicação do mundo real, precisamos garantir que o sistema sob teste (System Under Test - SUT) seja colocado em um estado conhecido e isolado antes que nossos métodos de teste sejam executados. Fazemos isso utilizando métodos de configuração (setup methods).
Utilizando Métodos de Configuração (Setup Methods)
O JUnit Jupiter oferece dois tipos diferentes de métodos de configuração, descritos a seguir:
Primeiro, o JUnit possui um método de setup que é invocado antes de cada método de teste. Esse método é tipicamente utilizado para criar e configurar o sistema sob teste (SUT). Isso significa que:
- Devemos instanciar as dependências do objeto testado dentro deste método. Em outras palavras, se quisermos usar dublês de teste (test doubles, como mocks ou stubs) em nossos testes unitários, devemos criá-los ou reiniciá-los aqui.
- Devemos criar a instância do próprio objeto testado neste método.
Podemos criar esse método de configuração seguindo estes passos:
- Adicionar uma função à classe de teste e garantir que ela não retorne nenhum valor (
Unit). - Anotar a função com a anotação
@BeforeEach(que substituiu a antiga anotação@Beforedo JUnit 4). Essa anotação informa ao JUnit que a função em questão deve ser executada antes de cada função anotada com@Test.
@BeforeEach não deve retornar nenhum valor e não pode ser private. Diferente do JUnit 4, o JUnit Jupiter permite injeção de parâmetros em métodos @BeforeEach caso você utilize extensões (como TestInfo ou @TempDir).
Vamos adicionar um novo método de configuração à nossa classe de teste. Este método imprime a string 'Invoked before each test method' no console.
Depois de adicionarmos este método à nossa classe de teste, seu código-fonte ficará assim:
|
|
Segundo, o JUnit possui um método de setup que é invocado uma única vez antes que qualquer método de teste da classe seja executado. Esse método é tipicamente usado para realizar configurações pesadas ou intensivas em recursos que são compartilhadas por vários métodos de teste. Por exemplo, se precisarmos iniciar um servidor em memória ou abrir uma conexão compartilhada, devemos fazer isso neste método.
Podemos criar esse método de configuração único seguindo estes passos:
- Adicionar um método estático (em Kotlin, dentro de um
companion objectanotado com@JvmStatic) à classe de teste, garantindo que ele não retorne nada (Unit). (Alternativamente, no JUnit Jupiter, você pode anotar a classe com@TestInstance(TestInstance.Lifecycle.PER_CLASS)para usar métodos normais sem precisar decompanion object). - Anotar o método com a anotação
@BeforeAll(que substituiu a antiga@BeforeClassdo JUnit 4). Essa anotação instrui o JUnit a invocar este método de setup uma única vez antes da execução de todos os métodos de teste da classe.
PER_METHOD), o JUnit Jupiter exige que o método anotado com @BeforeAll seja estático (@JvmStatic na JVM) e não retorne nenhum valor.
Vamos adicionar esse segundo método de configuração à nossa classe de teste. Este método imprime a string 'Invoked once before all test methods' no console.
Depois de adicionarmos este método à nossa classe de teste, seu código-fonte terá a seguinte aparência:
|
|
Agora já sabemos como utilizar os métodos de configuração suportados pelo JUnit Jupiter. Às vezes, reservamos recursos externos em nossos métodos de setup. Quando fazemos isso, naturalmente precisamos liberar esses recursos após a execução. Fazemos isso utilizando métodos de limpeza (teardown methods).
Utilizando Métodos de Limpeza (Teardown Methods)
O JUnit Jupiter oferece dois tipos diferentes de métodos de limpeza, descritos a seguir:
Primeiro, o JUnit possui um método de teardown que é invocado após cada método de teste. Podemos utilizar esse método quando precisamos liberar recursos externos ou limpar estados que foram alocados no método anotado com @BeforeEach.
Podemos criar esse método de limpeza seguindo estes passos:
- Adicionar uma função à nossa classe de teste e garantir que ela não retorne nenhum valor (
Unit). - Anotar a função com a anotação
@AfterEach(que substituiu a antiga@Afterdo JUnit 4). Essa anotação informa ao JUnit que a função em questão deve ser executada após cada função anotada com@Test.
@AfterEach não seja privado e não retorne nenhum valor (Unit). Ele é executado mesmo que o método @Test tenha falhado ou lançado uma exceção.
Vamos adicionar um novo método de limpeza à nossa classe de teste. Este método imprime a string 'Invoked after each test method' no console.
Depois de adicionarmos este método à nossa classe de teste, seu código-fonte ficará assim:
|
|
Segundo, o JUnit possui um método de teardown que é invocado uma única vez após todos os métodos de teste terem sido executados. Podemos usar esse método quando queremos liberar recursos externos compartilhados que foram reservados no método anotado com @BeforeAll.
Podemos criar esse método de limpeza final seguindo estes passos:
- Adicionar um método estático (
@JvmStaticdentro docompanion object) à classe de teste e garantir que o método não retorne nenhum valor (Unit). - Anotar o método com a anotação
@AfterAll(que substituiu a antiga@AfterClassdo JUnit 4). Essa anotação informa ao JUnit que este método de limpeza deve ser invocado uma única vez depois que todos os métodos de teste da classe tiverem terminado.
@BeforeAll, no modo de ciclo de vida padrão o método anotado com @AfterAll deve ser estático (@JvmStatic) e não retornar nenhum valor.
Vamos adicionar esse segundo método de limpeza à nossa classe de teste. Este método imprime a string 'Invoked once after all test methods' no console.
Depois de adicionarmos este método à nossa classe de teste, seu código-fonte completo ficará assim:
|
|
Agora já sabemos como adicionar métodos de teste à nossa classe e como utilizar todos os métodos de setup e teardown suportados pelo JUnit Jupiter.
Resta apenas uma pergunta importante:
O JUnit realmente executa os métodos encontrados em nossa classe de teste na ordem descrita?
Vamos avançar e descobrir o que acontece quando executamos nossos testes unitários.
Executando Nossos Testes Unitários
Quando executamos nossa classe de teste SetUpAndTearDownTest (via IDE ou rodando ./gradlew test), veremos a seguinte saída no console:
|
|
Como podemos comprovar pela saída acima, o JUnit Jupiter invoca os métodos exatamente na ordem previsível e isolada que descrevemos ao longo desta aula. Vamos resumir o que aprendemos.
Resumo
Esta aula nos ensinou quatro pontos fundamentais:
- No JUnit Jupiter 6 com Kotlin, classes de teste não precisam do modificador
publicexplícito (ao contrário do antigo JUnit 4), e adotam por convenção o sufixoTest. - Todos os métodos de teste são anotados com
@Test, não podem ser privados e não devem retornar nenhum valor (Unit). - O método anotado com
@BeforeEach(antigo@Before) é executado antes de cada teste, e o método anotado com@AfterEach(antigo@After) é executado após cada teste. - O método anotado com
@BeforeAll(antigo@BeforeClass) é executado uma única vez antes de todos os testes da classe, e o método anotado com@AfterAll(antigo@AfterClass) é executado uma única vez após todos os testes; no ciclo de vida padrão, ambos ficam em umcompanion objectcom@JvmStatic(ou em métodos regulares caso a classe use@TestInstance(Lifecycle.PER_CLASS)).