Orquestrando Testes de Integração com Extensões do Spring
Depois de concluirmos esta aula, nós:
- Compreenderemos como o módulo
spring-testintegra o Spring TestContext Framework ao modelo de extensões do JUnit Jupiter por meio daSpringExtension. - Saberemos como configurar testes de integração tanto com anotações explícitas (
@ExtendWith(SpringExtension::class)) quanto com anotações compostas (@SpringJUnitConfig,@SpringJUnitWebConfige@SpringBootTest). - Saberemos como escrever testes de integração para controladores Spring MVC que consultam o banco de dados tanto usando uma estrutura plana tradicional quanto usando uma estrutura hierárquica com
@Nested. - Saberemos como criar nossas próprias meta-anotações compostas para eliminar a repetição de anotações em classes
@Nested.
Vamos começar revisando as dependências necessárias.
1. Obtendo as Dependências Necessárias
Historicamente, quando o JUnit 5 foi lançado antes do Spring Framework 5, era necessário adicionar uma biblioteca externa experimental (spring-test-junit5 de Sam Brannen) para integrar o Spring TestContext Framework ao JUnit Jupiter.
A partir do Spring Framework 5 — e nativamente no Spring Framework 7 / Spring Boot 4.1.2 com JUnit 6.0.3 — a extensão SpringExtension faz parte diretamente do artefato oficial spring-test!
- Name: Spring TestContext Framework
- Group ID:
org.springframework - Artifact ID:
spring-test
Quando utilizamos spring-boot-starter-test (ou spring-boot-starter-webmvc-test) no nosso build.gradle.kts, tanto spring-test quanto junit-jupiter já estão prontos para uso:
|
|
A seguir, vamos conhecer o controlador que será o nosso sistema sob teste (SUT).
2. Conhecendo o Sistema Sob Teste (SUT)
Durante esta aula, escreveremos testes de integração para uma classe chamada MessageController. Ela possui um único endpoint que processa requisições HTTP GET enviadas para o caminho /api/message, consulta todas as mensagens salvas no banco de dados relacional e retorna a lista serializada como um array de objetos JSON:
|
|
Caso nenhuma mensagem seja encontrada no banco de dados, nosso endpoint retorna uma lista JSON vazia ([]) com status 200 OK:
|
|
Vamos ver agora como configurar nossa classe de teste de integração com a extensão do Spring para o JUnit Jupiter.
3. Configurando a Classe de Teste com SpringExtension
Para habilitar o suporte ao Spring TestContext Framework em um teste JUnit 6, precisamos registrar a extensão SpringExtension. Existem três maneiras de fazer isso, da mais explícita (baixo nível) até a mais concisa (alto nível):
Abordagem 1: Configuração Explícita com @ExtendWith(SpringExtension::class)
Em uma aplicação Spring MVC tradicional sem auto-configuração do Spring Boot, combinaríamos @ExtendWith(SpringExtension::class) com @ContextConfiguration e @WebAppConfiguration:
|
|
Abordagem 2: Anotação Composta @SpringJUnitWebConfig
O próprio spring-test oferece duas anotações compostas (composed annotations) que já incluem @ExtendWith(SpringExtension::class) internamente:
@SpringJUnitConfig: combina@ExtendWith(SpringExtension::class)+@ContextConfiguration.@SpringJUnitWebConfig: combina@ExtendWith(SpringExtension::class)+@ContextConfiguration+@WebAppConfiguration.
Com @SpringJUnitWebConfig, podemos simplificar a declaração acima para:
|
|
Abordagem 3: @SpringBootTest + @AutoConfigureMockMvc (Padrão Moderno no Spring Boot 4)
Em aplicações Spring Boot 4.1.2, todas as anotações de teste do Spring Boot (@SpringBootTest, @WebMvcTest, @DataJpaTest) já são meta-anotadas com @ExtendWith(SpringExtension::class)! Isso significa que você não precisa declarar @ExtendWith(SpringExtension::class) manualmente quando usa @SpringBootTest.
Vamos escrever nossa classe FindAllMessagesIT configurando o MockMvc e populando o banco de dados com um script SQL (messages.sql) que insere a mensagem de ID 1 e texto "Hello World!":
|
|
E um script cleardb.sql para limpar a tabela nos cenários vazios:
|
|
4. Escrevendo Testes de Integração Planos (Flat Structure)
Primeiro, vamos escrever nossa classe FindAllMessagesIT utilizando a estrutura tradicional (plana) de métodos @Test. Testaremos três verificações para cada um dos dois cenários (quando nenhuma mensagem é encontrada e quando uma mensagem é encontrada):
- O status HTTP retornado deve ser
200 OK. - O
Content-Typeda resposta deve serapplication/json. - O corpo JSON retornado deve conter a lista esperada de mensagens.
|
|
[!NOTE] Observe também como o JUnit Jupiter +
SpringExtensionpermite injeção de dependência diretamente no construtor da classe de teste (class FindAllMessagesIT(@Autowired private val mockMvc: MockMvc)). Isso torna a propriedademockMvcimutável (val) e elimina a necessidade delateinit var!
Embora os testes acima sejam fáceis de entender, note que tivemos que repetir @Sql("/com/example/message/messages.sql") e o sufixo WhenOneMessageIsFound em três métodos diferentes. Vamos ver como melhorar essa estrutura usando @Nested.
5. Estruturando Testes de Integração Hierárquicos com @Nested
Com a anotação @Nested do JUnit 6, podemos agrupar os casos de teste de cada estado do banco de dados em uma inner class dedicada (WhenNoMessagesIsFound e WhenOneMessageIsFound) e colocar a anotação @Sql diretamente na classe @Nested!
[!TIP] No Spring Framework moderno (5.0+ até o Spring 7), a
SpringExtensionpropaga automaticamente a configuração do contexto (@SpringBootTest,@ContextConfiguration,@Sql) da classe externa para todas as classes internas@Nested. Caso você utilize@SpringJUnitWebConfigem contextos legados específicos que exijam redeclaração, podemos criar uma meta-anotação composta para evitar qualquer duplicação.
Vamos refatorar nossa classe FindAllMessagesIT utilizando @Nested:
|
|
6. Criando Meta-Anotações Compostas para Testes de Integração
Uma das funcionalidades mais poderosas da combinação entre JUnit 6 e Spring é o suporte de primeira classe a meta-anotações (composed annotations).
Se dezenas de classes de teste de integração da nossa aplicação repetem o mesmo conjunto de anotações de infraestrutura (@SpringBootTest, @AutoConfigureMockMvc, @ActiveProfiles("integrationTest"), etc.), podemos agrupá-las em uma única anotação customizada do nosso projeto, como @IntegrationTest ou @NestedSpringWebConfig:
|
|
Agora, qualquer classe de teste de integração web da nossa suíte precisa apenas de @WebIntegrationTest:
|
|
Resumo
Nesta aula bônus, aprendemos como o Spring se integra ao JUnit 6:
- A extensão
SpringExtension(incluída nospring-test) conecta o ciclo de vida do Spring TestContext Framework ao JUnit Jupiter. - Graças ao
ParameterResolverimplementado pelaSpringExtension, podemos injetar beans do Spring (comoMockMvc,JdbcTemplateou repositórios) diretamente no construtor da nossa classe de teste. - Utilizando classes
@Nestedcombinadas com@Sqlem nível de classe interna, eliminamos a repetição de scripts de preparação de banco e criamos relatórios de teste de integração legíveis e organizados por estado do banco de dados. - Com meta-anotações customizadas (como
@WebIntegrationTest), padronizamos toda a configuração de infraestrutura da nossa suíte em uma única anotação semântica.