Modelo de Extensões do JUnit 6 (@ExtendWith e @TempDir)

Ao concluirmos esta aula, nós:

  • Saberemos o que era uma Rule (@Rule) no JUnit 4 e como ela evoluiu para uma Extensão (Extension) no JUnit Jupiter 6.
  • Seremos capazes de registrar e utilizar extensões do JUnit 6 em nossas classes de teste.
  • Seremos capazes de criar arquivos e diretórios temporários que são automaticamente excluídos após a execução de um método de teste utilizando @TempDir (substituindo a antiga TemporaryFolder rule).

O Que É uma Rule / Extensão no JUnit?

No JUnit, uma Rule (no JUnit 4) ou Extensão (no JUnit Jupiter 6) é um componente que intercepta chamadas de métodos de teste e nos permite executar ações customizadas antes que um método de teste seja iniciado, durante sua resolução de parâmetros e depois que o método de teste tiver terminado sua execução.

No JUnit 4 clássico, todas as rules precisavam implementar a interface org.junit.rules.TestRule. No JUnit Jupiter 6, o conceito de @Rule e @ClassRule foi unificado sob a interface base org.junit.jupiter.api.extension.Extension e suas interfaces de ciclo de vida especializadas (como BeforeEachCallback, AfterEachCallback, ParameterResolver).

Podemos fazer coisas muito úteis com extensões e recursos nativos do JUnit 6. Por exemplo, o JUnit Jupiter fornece mecanismos que podem:

  • Criar arquivos e diretórios temporários que são automaticamente limpos e excluídos após a execução de um método de teste (@TempDir).
  • Falhar um teste automaticamente se o tempo limite especificado for excedido antes do término do método (@Timeout).
  • Configurar um recurso externo, como um socket, servidor HTTP (WireMock) ou contêiner de banco de dados (Testcontainers) antes do teste e liberar o recurso automaticamente ao final.

A seguir, descobriremos como ativar e utilizar extensões em nossas classes de teste.


Utilizando Extensões no JUnit 6 (Substituindo @Rule)

No JUnit 4, para usar uma rule como FooBarRule, éramos obrigados a seguir dois passos burocráticos:

  1. Adicionar um campo public não-estático na classe de teste cujo tipo fosse subtipo de TestRule.
  2. Anotar esse campo com @Rule.
1
2
3
4
5
// Legado JUnit 4 (Exigia campo público mutável ou @get:Rule no Kotlin):
// class RuleTest {
//     @get:Rule
//     val rule = FooBarRule()
// }
Nota Histórica: No JUnit 4, o campo anotado com @Rule obrigatoriamente precisava ser public e não podia ser static (caso contrário, exigia @ClassRule). Em Kotlin, como propriedades geram campos privados com getters públicos por padrão, isso exigia o uso da sintaxe @get:Rule ou @JvmField, o que frequentemente confundia desenvolvedores.

No JUnit Jupiter 6, essa limitação desapareceu! Podemos ativar uma extensão de duas formas muito mais limpas:

  1. Forma Declarativa (@ExtendWith): Anotamos a classe (ou método) diretamente com @ExtendWith(FooBarExtension::class).
  2. Forma Programática (@RegisterExtension): Caso precisemos passar parâmetros no construtor da extensão, declaramos uma propriedade na classe anotada com @RegisterExtension (que não precisa ser public!).

Suponha que queiramos usar a extensão FooBarExtension em nossa classe de teste. Depois de habilitá-la no JUnit 6, o código-fonte da nossa classe fica assim:

1
2
3
4
5
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(FooBarExtension::class)
class ExtensionTest {
}

Ou, se precisarmos configurar a instância programaticamente com @RegisterExtension:

1
2
3
4
5
6
7
8
import org.junit.jupiter.api.extension.RegisterExtension

class ExtensionTest {

    @JvmField
    @RegisterExtension
    val rule = FooBarExtension()
}

Vamos colocar esse conhecimento em prática e descobrir como podemos criar arquivos e diretórios temporários que são excluídos automaticamente após a execução de um método de teste.


Utilizando a Extensão Nativa @TempDir (Substituindo a TemporaryFolder Rule)

Quando precisamos criar arquivos e/ou diretórios que devem ser excluídos após a execução de um método de teste, o JUnit 4 exigia instanciar a rule TemporaryFolder.

No JUnit Jupiter 6, essa funcionalidade já vem embutida nativamente através da anotação @TempDir (org.junit.jupiter.api.io.TempDir). Para habilitá-la, podemos anotar uma propriedade da classe (lateinit var tmpFolder: Path ou File) ou injetá-la diretamente como parâmetro do método @Test!

Depois de habilitarmos o @TempDir em nossa classe de teste, o código-fonte fica assim:

1
2
3
4
5
6
7
8
import org.junit.jupiter.api.io.TempDir
import java.io.File

class TempDirTest {

    @TempDir
    lateinit var tmpFolder: File
}

Agora podemos criar arquivos e diretórios dentro de tmpFolder. Vamos criar um teste simples que cria um arquivo chamado file.txt que será automaticamente excluído pelo JUnit assim que o método de teste terminar.

Em outras palavras, adicionaremos um novo método de teste à nossa classe implementando estes três passos:

  1. Criar um novo arquivo file.txt dentro do diretório temporário tmpFolder e garantir sua criação no sistema de arquivos (createNewFile()).
  2. Garantir que o objeto File criado é realmente um arquivo (isFile).
  3. Garantir que o arquivo foi criado exatamente dentro do diretório raiz da nossa pasta temporária (tmpFolder).

Depois de implementarmos este método de teste, o código-fonte completo da nossa classe de teste ficará assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.io.File

class TempDirTest {

    @TempDir
    lateinit var tmpFolder: File

    @Test
    fun shouldCreateNewFileInTemporaryFolder() {
        val created = File(tmpFolder, "file.txt").apply {
            createNewFile()
        }

        assertTrue(created.isFile)
        assertEquals(tmpFolder, created.parentFile)
    }
}

(Dica de Kotlin + NIO.2: Com @TempDir, você também pode usar java.nio.file.Path em vez de java.io.File, o que permite criar o arquivo de forma ainda mais concisa com Files.createFile(tmpDir.resolve("file.txt"))).

Política de Limpeza (CleanupMode) e Garantia de Deleção:

Na antiga TemporaryFolder rule do JUnit 4 (antes da versão 4.13), o JUnit não garantia que uma falha ao deletar os arquivos temporários causaria falha no teste (exigindo configurar manualmente TemporaryFolder.builder().assureDeletion().build()).

No JUnit Jupiter 6, o @TempDir já verifica a exclusão por padrão e permite configurar explicitamente o modo de limpeza (cleanup), por exemplo para manter os arquivos apenas se o teste falhar (ótimo para diagnóstico):
1
2
3
4
5
6
7
8
9
import org.junit.jupiter.api.io.CleanupMode
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Path

class TempDirDiagnosticTest {

    @TempDir(cleanup = CleanupMode.ON_SUCCESS)
    lateinit var tmpDir: Path
}

Vamos resumir o que aprendemos nesta aula.


Resumo

Esta aula nos ensinou quatro pontos essenciais:

  • Uma Rule (no JUnit 4) ou Extensão (no JUnit 6) é um componente que intercepta o ciclo de vida dos métodos de teste, permitindo executar lógicas antes e depois de cada teste.
  • O JUnit 6 unificou as antigas @Rule e @ClassRule sob o modelo de extensões com @ExtendWith e @RegisterExtension.
  • Ao contrário do JUnit 4 (que exigia campos public não-estáticos com @Rule), o JUnit 6 permite injetar extensões declarativamente na classe, em propriedades ou diretamente nos parâmetros do método de teste.
  • A extensão nativa @TempDir (substituta da antiga TemporaryFolder rule) permite criar arquivos e diretórios temporários com File ou Path que são automaticamente removidos após a execução do teste.