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 antigaTemporaryFolderrule).
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:
- Adicionar um campo
publicnão-estático na classe de teste cujo tipo fosse subtipo deTestRule. - Anotar esse campo com
@Rule.
|
|
@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:
- Forma Declarativa (
@ExtendWith): Anotamos a classe (ou método) diretamente com@ExtendWith(FooBarExtension::class). - 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 serpublic!).
Para ilustrar na prática como uma extensão customizada intercepta o ciclo de vida dos testes medindo o tempo decorrido, implementamos a extensão TimingExtension:
|
|
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:
|
|
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:
- Criar um novo arquivo
file.txtdentro do diretório temporáriotmpFoldere garantir sua criação no sistema de arquivos (createNewFile()). - Garantir que o objeto
Filecriado é realmente um arquivo (isFile). - Garantir que o arquivo foi criado exatamente dentro do diretório raiz da nossa pasta temporária (
tmpFolder).
Depois de implementarmos nossa classe de testes integrando @ExtendWith(TimingExtension::class) e @TempDir (demonstrando tanto o suporte a java.io.File quanto a java.nio.file.Path), o código-fonte sincronizado com a nossa suíte de testes fica assim:
|
|
(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"))).
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):
|
|
Executando Nossos Testes com Extensões e @TempDir
Quando executamos a suíte de testes no Gradle com a extensão customizada TimingExtension e o @TempDir, observamos a medição transparente do tempo de execução de cada teste pelo interceptador e a deleção automática e segura das pastas temporárias:
Initialized native services in: /home/wc/.gradle/native
Initialized jansi services in: /home/wc/.gradle/native
The client will now receive all logging from the daemon (pid: 15302, address: [fe80::1%1:54325]).
Starting Build
Settings evaluated using settings file '/home/wc/test-with-spring/code/01-starter-junit6/settings.gradle.kts'.
Projects loaded. Root project using build file '/home/wc/test-with-spring/code/01-starter-junit6/build.gradle.kts'.
Included projects: [root project 'starter-junit6']
> Configure project :
Evaluating root project 'starter-junit6' using build file '/home/wc/test-with-spring/code/01-starter-junit6/build.gradle.kts'.
Using Kotlin Gradle Plugin 2.4.21
Using Java Toolchain: Temurin 21.0.6 (/home/wc/.sdkman/candidates/java/21.0.6-tem)
Spring Boot 4.1.2 plugin applied.
All projects evaluated.
Selected primary task 'test' from project :
> Task :compileKotlin UP-TO-DATE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :compileTestKotlin
Compiling with Kotlin compiler version 2.4.21 (JVM target 21)
Compiling Kotlin files from [/home/wc/test-with-spring/code/01-starter-junit6/src/test/kotlin]
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
Starting process 'Gradle Test Executor 1'. Working directory: /home/wc/test-with-spring/code/01-starter-junit6 Command: /home/wc/.sdkman/candidates/java/21.0.6-tem/bin/java -ea -cp ...
Successfully started process 'Gradle Test Executor 1'
com.testwithspring.starter.lifecycle.ExtensionModelTest > shouldCreateNewFileInTemporaryFolder() STANDARD_OUT
Method [shouldCreateNewFileInTemporaryFolder] took 4 ms.
com.testwithspring.starter.lifecycle.ExtensionModelTest > shouldCreateNewFileInTemporaryFolder() PASSED
com.testwithspring.starter.lifecycle.ExtensionModelTest > shouldCreateAndVerifyFileUsingPathAndNio() STANDARD_OUT
Method [shouldCreateAndVerifyFileUsingPathAndNio] took 2 ms.
com.testwithspring.starter.lifecycle.ExtensionModelTest > shouldCreateAndVerifyFileUsingPathAndNio() PASSED
ExtensionModelTest (@ExtendWith TimingExtension & @TempDir) > 2 tests completed, 0 failed, 0 skipped
Finished generating test XML results (0.009 secs) into: /home/wc/test-with-spring/code/01-starter-junit6/build/test-results/test
Generating HTML test report...
Finished generating test html results (0.016 secs) into: /home/wc/test-with-spring/code/01-starter-junit6/build/reports/tests/test
BUILD SUCCESSFUL in 1s
5 actionable tasks: 2 executed, 3 up-to-date
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
@Rulee@ClassRulesob o modelo de extensões com@ExtendWithe@RegisterExtension. - Ao contrário do JUnit 4 (que exigia campos
publicnã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 antigaTemporaryFolderrule) permite criar arquivos e diretórios temporários comFileouPathque são automaticamente removidos após a execução do teste.