Configurando o ApplicationContext com @SpringBootTest

Ao concluirmos esta aula, nós:

  • Seremos capazes de identificar os três principais recursos do suporte a testes de integração do Spring TestContext Framework.
  • Saberemos como configurar o ApplicationContext dos nossos testes de integração ao testar uma aplicação Spring Framework tradicional (@ExtendWith(SpringExtension::class) e @ContextConfiguration).
  • Seremos capazes de configurar o ApplicationContext dos nossos testes de integração ao testar uma aplicação Spring Boot 4 (@SpringBootTest).

Os Principais Recursos do Suporte a Testes de Integração do Spring Test Framework

O suporte a testes de integração do Spring Test Framework (Spring TestContext Framework) possui três recursos fundamentais que tornam nossos testes rápidos, limpos e confiáveis:

Primeiro, ele suporta injeção de dependências diretamente nas classes de teste: Isso significa que podemos configurar o sistema sob teste (SUT) utilizando classes de configuração (@Configuration ou @SpringBootApplication) e injetar os beans do Spring diretamente nas nossas classes de teste (via @Autowired ou diretamente via construtor primário no JUnit Jupiter 6 com Kotlin!). Assim, reutilizamos a configuração real da aplicação e evitamos código de inicialização manual repetitivo.

Segundo, ele faz cache dos ApplicationContexts carregados: Se criássemos e inicializássemos todos os componentes do Spring (pool de conexões HikariCP, EntityManagerFactory do Hibernate, filtros web, etc.) do zero antes de cada método de teste, nossa suíte de testes de integração seria extremamente lenta.

O Spring Test resolve esse problema mantendo um cache em memória do ApplicationContext após carregá-lo pela primeira vez. Quando outras classes de teste de integração solicitam a mesma configuração de contexto, o Spring Test reutiliza instantaneamente o ApplicationContext em cache!

Terceiro, ele fornece suporte integrado a gerenciamento de transações: Quando escrevemos testes que interagem com um armazenamento persistente como um banco de dados relacional, precisamos resolver dois problemas:

  • Garantir que as alterações feitas no banco de dados durante um teste não afetem os testes subsequentes.
  • Executar operações que exigem uma transação ativa (como métodos anotados com @Transactional ou o EntityManager do JPA).

Por padrão, quando anotamos um teste de integração com @Transactional, o Spring Test inicia uma transação antes do método de teste e realiza o rollback automático ao final do teste!


Introdução ao Sistema Sob Teste (SUT)

Nosso sistema sob teste de exemplo consiste em duas classes simples:

Primeiro, a classe MessageRepository anotada com @Repository, que possui um único método chamado findMessage() que retorna a string "Hello World!":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package com.testwithspring.intermediate.message

import org.springframework.stereotype.Repository

@Repository
class MessageRepository {

    fun findMessage(): String {
        return "Hello World!"
    }
}

Segundo, a classe MessageService anotada com @Service, que recebe o MessageRepository via injeção de dependência no construtor e expõe o método getMessage():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.testwithspring.intermediate.message

import org.springframework.stereotype.Service

@Service
class MessageService(
    private val repository: MessageRepository
) {

    fun getMessage(): String {
        return repository.findMessage()
    }
}

Estamos prontos para configurar o ApplicationContext do nosso primeiro teste de integração!


Configurando o ApplicationContext dos Nossos Testes de Integração

Vamos aprender como configurar o ApplicationContext dos nossos testes de integração em dois cenários: usando o Spring Framework puro e usando o Spring Boot 4.

1. Usando o Spring Framework (@ExtendWith(SpringExtension::class) + @ContextConfiguration)

Se estivermos escrevendo testes de integração para uma aplicação que utiliza o Spring Framework puro (sem Spring Boot), configuramos o ApplicationContext seguindo dois passos:

Primeiro, garantimos que nossa classe de teste seja executada com a extensão do Spring para o JUnit Jupiter 6. No antigo JUnit 4, usávamos @RunWith(SpringJUnit4ClassRunner::class). No JUnit Jupiter 6, usamos @ExtendWith(SpringExtension::class):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package com.testwithspring.intermediate.message

import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.test.context.junit.jupiter.SpringExtension

@ExtendWith(SpringExtension::class)
@Tag("integrationTest")
class GetMessageTest {
}

Segundo, especificamos as classes de configuração (@Configuration) que definem os beans do nosso ApplicationContext anotando nossa classe de teste com @ContextConfiguration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.testwithspring.intermediate.message

import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit.jupiter.SpringExtension

@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [MessageContext::class])
@Tag("integrationTest")
class GetMessageTest {
}

Dica de Atalho com @SpringJUnitConfig: No Spring Framework moderno (5.0+ até o Spring Framework 7), a anotação composta @SpringJUnitConfig(classes = [MessageContext::class]) combina @ExtendWith(SpringExtension::class) e @ContextConfiguration em uma única anotação!

Onde MessageContext é uma classe de configuração simples que habilita o escaneamento de componentes (@ComponentScan):

1
2
3
4
5
6
7
8
package com.testwithspring.intermediate.message

import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration

@Configuration
@ComponentScan
class MessageContext

(Nota: Caso você trabalhe em um sistema legado que ainda utilize arquivos XML de configuração do Spring, basta usar o atributo locations de @ContextConfiguration(locations = ["classpath:message-context.xml"])).

Agora podemos injetar o bean MessageService na nossa classe de teste e verificar que o método getMessage() retorna "Hello World!":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.testwithspring.intermediate.message

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit.jupiter.SpringExtension

@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [MessageContext::class])
@Tag("integrationTest")
class GetMessageTest(
    @Autowired private val service: MessageService
) {

    @Test
    fun `deve retornar a mensagem Hello World`() {
        val message = service.getMessage()
        assertThat(message).isEqualTo("Hello World!")
    }
}

Injeção por Construtor em Testes Kotlin: Graças à integração do SpringExtension com o ParameterResolver do JUnit Jupiter 6, em Kotlin podemos declarar @Autowired private val service: MessageService diretamente no construtor da classe de teste (ou configurar spring.test.constructor.autowire.mode=all no junit-platform.properties para nem precisar escrever @Autowired!), evitando propriedades lateinit var mutáveis!

2. Usando o Spring Boot 4 (@SpringBootTest)

Se estivermos escrevendo testes de integração para uma aplicação Spring Boot, devemos substituir @ContextConfiguration pela anotação @SpringBootTest.

Por que usar @SpringBootTest?

  1. Ela carrega o contexto completo do Spring Boot (incluindo auto-configurações, application.properties / application.yml, conversores de tipos e infraestrutura de servidor web se necessário).
  2. No JUnit Jupiter 6, a própria anotação @SpringBootTest já inclui @ExtendWith(SpringExtension::class) como meta-anotação! Portanto, você não precisa repetir @ExtendWith(SpringExtension::class) no topo da classe.

Veja como fica a nossa classe de teste GetMessageTest usando @SpringBootTest:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package com.testwithspring.intermediate.message

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest(classes = [MessageApplication::class])
@Tag("integrationTest")
class GetMessageTest(
    @Autowired private val service: MessageService
) {

    @Test
    fun `deve retornar a mensagem Hello World`() {
        val message = service.getMessage()
        assertThat(message).isEqualTo("Hello World!")
    }
}

Onde MessageApplication é a classe principal da nossa aplicação Spring Boot:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package com.testwithspring.intermediate.message

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class MessageApplication

fun main(args: Array<String>) {
    runApplication<MessageApplication>(*args)
}

(Nota: Se a sua classe de teste estiver no mesmo pacote ou em um subpacote de MessageApplication, você pode escrever apenas @SpringBootTest sem especificar classes = [...], pois o Spring Boot localiza automaticamente a classe anotada com @SpringBootApplication!).


Resumo

Esta aula nos ensinou três conceitos fundamentais:

  • No JUnit Jupiter 6, a extensão SpringExtension (@ExtendWith(SpringExtension::class)) substitui o antigo SpringJUnit4ClassRunner e habilita injeção de dependências, cache de contexto e gerenciamento de transações nos nossos testes.
  • Ao testar uma aplicação que usa o Spring Framework puro, configuramos as classes @Configuration ou arquivos XML usando @ContextConfiguration (ou @SpringJUnitConfig).
  • Ao testar uma aplicação que usa o Spring Boot, utilizamos a anotação @SpringBootTest, que já registra o SpringExtension automaticamente e carrega os recursos específicos do Spring Boot.