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:

1
2
3
class SetUpAndTearDownTest {

}
Nota sobre Visibilidade (JUnit 4 vs JUnit Jupiter 6 e Kotlin): No JUnit 4 clássico com Java, todas as classes de teste eram obrigadas a ter o modificador explícito 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:

  1. 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 seja Unit, o padrão implícito do Kotlin).
  2. Anotar a função com a anotação @Test do pacote org.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.
Nota: O JUnit Jupiter exige que métodos anotados com @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:

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

class SetUpAndTearDownTest {

    @Test
    fun testOne() {
        println("Test One")
    }

    @Test
    fun testTwo() {
        println("Test Two")
    }
}
Atenção — Problemas Encontrados Nesta Classe Inicial:

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:

  1. Adicionar uma função à classe de teste e garantir que ela não retorne nenhum valor (Unit).
  2. Anotar a função com a anotação @BeforeEach (que substituiu a antiga anotação @Before do 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.
Nota: No JUnit Jupiter, o método anotado com @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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class SetUpAndTearDownTest {

    @BeforeEach
    fun beforeEachTestMethod() {
        println("Invoked before each test method")
    }

    @Test
    fun testOne() {
        println("Test One")
    }

    @Test
    fun testTwo() {
        println("Test Two")
    }
}

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:

  1. Adicionar um método estático (em Kotlin, dentro de um companion object anotado 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 de companion object).
  2. Anotar o método com a anotação @BeforeAll (que substituiu a antiga @BeforeClass do 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.
Nota: Por padrão (ciclo de vida 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class SetUpAndTearDownTest {

    companion object {
        @BeforeAll
        @JvmStatic
        fun beforeAllTestMethods() {
            println("Invoked once before all test methods")
        }
    }

    @BeforeEach
    fun beforeEachTestMethod() {
        println("Invoked before each test method")
    }

    @Test
    fun testOne() {
        println("Test One")
    }

    @Test
    fun testTwo() {
        println("Test Two")
    }
}

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:

  1. Adicionar uma função à nossa classe de teste e garantir que ela não retorne nenhum valor (Unit).
  2. Anotar a função com a anotação @AfterEach (que substituiu a antiga @After do 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.
Nota: O JUnit Jupiter exige que o método anotado com @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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class SetUpAndTearDownTest {

    companion object {
        @BeforeAll
        @JvmStatic
        fun beforeAllTestMethods() {
            println("Invoked once before all test methods")
        }
    }

    @BeforeEach
    fun beforeEachTestMethod() {
        println("Invoked before each test method")
    }

    @AfterEach
    fun afterEachTestMethod() {
        println("Invoked after each test method")
    }

    @Test
    fun testOne() {
        println("Test One")
    }

    @Test
    fun testTwo() {
        println("Test Two")
    }
}

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:

  1. Adicionar um método estático (@JvmStatic dentro do companion object) à classe de teste e garantir que o método não retorne nenhum valor (Unit).
  2. Anotar o método com a anotação @AfterAll (que substituiu a antiga @AfterClass do 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.
Nota: Assim como no @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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class SetUpAndTearDownTest {

    companion object {
        @BeforeAll
        @JvmStatic
        fun beforeAllTestMethods() {
            println("Invoked once before all test methods")
        }

        @AfterAll
        @JvmStatic
        fun afterAllTestMethods() {
            println("Invoked once after all test methods")
        }
    }

    @BeforeEach
    fun beforeEachTestMethod() {
        println("Invoked before each test method")
    }

    @AfterEach
    fun afterEachTestMethod() {
        println("Invoked after each test method")
    }

    @Test
    fun testOne() {
        println("Test One")
    }

    @Test
    fun testTwo() {
        println("Test Two")
    }
}

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:

1
2
3
4
5
6
7
8
Invoked once before all test methods
Invoked before each test method
Test One
Invoked after each test method
Invoked before each test method
Test Two
Invoked after each test method
Invoked once after all test methods

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 public explícito (ao contrário do antigo JUnit 4), e adotam por convenção o sufixo Test.
  • 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 um companion object com @JvmStatic (ou em métodos regulares caso a classe use @TestInstance(Lifecycle.PER_CLASS)).