Customizando o Contexto com @TestConfiguration e Profiles

Ao concluirmos esta aula, nós:

  • Compreenderemos por que é uma excelente prática utilizar configurações e beans customizados quando executamos nossos testes de integração.
  • Saberemos como utilizar diferentes arquivos de propriedades e diferentes beans do Spring em ambientes distintos (produção vs testes de integração).
  • Seremos capazes de especificar o perfil ativo do Spring (@ActiveProfiles) ao escrever testes de integração para nossa aplicação.

Por Que Customizar o Contexto nos Testes de Integração?

É muito comum que nossa aplicação utilize configurações diferentes em cada ambiente (desenvolvimento, homologação e produção). Quando escrevemos testes de integração, nosso objetivo ao customizar a configuração do ApplicationContext é substituir certos componentes reais de infraestrutura externa por componentes controlados que tornem nossos testes mais rápidos, determinísticos e fáceis de escrever.

Por exemplo, frequentemente queremos:

  • Tornar nossos testes o mais rápidos e isolados possível: Externalizando a configuração de banco de dados para apontar para um container efêmero do Testcontainers (ou um banco em memória).
  • Eliminar chamadas HTTP reais para serviços externos de terceiros: Não queremos que nossos testes de integração falhem porque a API de pagamentos ou de envio de e-mails de um fornecedor externo está fora do ar. Podemos resolver isso substituindo o cliente HTTP externo por um stub (ou apontando-o para um servidor WireMock local).
  • Congelar o tempo atual (Clock / DateTimeService): Testar regras de negócio que dependem da data e hora atual (Instant.now()) usando o relógio real do sistema operacional é receita certa para testes frágeis. Ao encapsularmos o relógio em um bean do Spring (Clock ou DateTimeService), podemos substituí-lo por um stub que retorna um instante fixo e determinístico durante os testes de integração!

Vamos aprender duas técnicas complementares para customizar o nosso ApplicationContext: perfis do Spring (@Profile + @ActiveProfiles) e @TestConfiguration.


Configurando os Perfis Disponíveis (object Profiles)

Quando utilizamos perfis do Spring (Spring profiles) para separar configurações de produção e de teste, a primeira boa prática é criar um object de constantes em Kotlin com os nomes dos perfis. Isso evita erros de digitação (typos) em strings mágicas espalhadas pelo código.

Nesta aula utilizaremos dois perfis:

  • application: Utilizado quando executamos nossa aplicação normalmente.
  • integrationTest: Utilizado quando executamos nossos testes de integração.

Veja como fica o nosso object Profiles:

1
2
3
4
5
6
package com.testwithspring.intermediate.config

object Profiles {
    const val APPLICATION = "application"
    const val INTEGRATION_TEST = "integrationTest"
}

Vamos descobrir agora como carregar arquivos .properties diferentes dependendo do perfil ativo!


Usando Arquivos de Propriedades Diferentes em Cada Ambiente

Tanto o Spring Framework puro quanto o Spring Boot oferecem suporte nativo para carregar arquivos de propriedades específicos por ambiente. Vamos examinar os dois casos.

1. Usando o Spring Framework Puro (@Profile + @PropertySource)

Em uma aplicação Spring Framework tradicional (sem Spring Boot), podemos configurar nossa classe principal @Configuration (ExampleApplicationContext) para:

  • Ler o arquivo classpath:application.properties quando o perfil application estiver ativo.
  • Ler o arquivo classpath:integration-test.properties quando o perfil integrationTest estiver ativo.

Fazemos isso declarando duas classes internas anotadas com @Configuration, @Profile e @PropertySource:

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

import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.context.annotation.Profile
import org.springframework.context.annotation.PropertySource

@Configuration
@Import(
    PersistenceContext::class,
    WebMvcContext::class
)
class ExampleApplicationContext {

    @Profile(Profiles.APPLICATION)
    @Configuration
    @PropertySource("classpath:application.properties")
    class ApplicationProperties

    @Profile(Profiles.INTEGRATION_TEST)
    @Configuration
    @PropertySource("classpath:integration-test.properties")
    class IntegrationTestProperties
}

2. Usando o Spring Boot 4 (application-{profile}.properties / .yml)

Se estivermos usando o Spring Boot, não precisamos escrever nenhuma classe @PropertySource! O Spring Boot carrega automaticamente arquivos de configuração específicos do perfil ativo seguindo a convenção de nomenclatura:

  • application-{profile}.properties (ou application-{profile}.yml) no diretório src/main/resources ou src/integration-test/resources.

Em outras palavras, usando os nossos dois perfis:

  • Quando o perfil application estiver ativo, o Spring Boot lerá as propriedades de application-application.properties (além do application.properties base).
  • Quando o perfil integrationTest estiver ativo nos nossos testes, o Spring Boot lerá e sobrescreverá as propriedades a partir do arquivo application-integrationTest.properties!

Usando Beans Diferentes em Ambientes Diferentes

Suponha que nossa aplicação possua a seguinte classe MessageService:

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

class MessageService(
    private val message: String
) {
    fun getMessage(): String = message
}

Se quisermos que o container do Spring injete uma instância de MessageService com a mensagem "Hello World!" quando a aplicação estiver rodando (Profiles.APPLICATION) e uma instância com a mensagem "Hello Test!" durante os testes de integração (Profiles.INTEGRATION_TEST), podemos usar a anotação @Profile nos métodos @Bean:

 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
package com.testwithspring.intermediate.config

import com.testwithspring.intermediate.message.MessageService
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.context.annotation.Profile

@Configuration
@Import(
    PersistenceContext::class,
    WebMvcContext::class
)
class ExampleApplicationContext {

    @Profile(Profiles.APPLICATION)
    @Bean
    fun appMessageService(): MessageService {
        return MessageService("Hello World!")
    }

    @Profile(Profiles.INTEGRATION_TEST)
    @Bean
    fun testMessageService(): MessageService {
        return MessageService("Hello Test!")
    }
}

Alternativa com @TestConfiguration no Spring Boot: Além de @Profile, o Spring Boot oferece a anotação @TestConfiguration (no pacote src/integration-test/kotlin). Classes anotadas com @TestConfiguration não são escaneadas automaticamente pelo @SpringBootApplication de produção, podendo ser importadas sob demanda nos testes via @Import(MyTestConfig::class) com @Primary para substituir beans específicos (como um Clock.fixed(...) ou um stub de serviço externo) sem poluir as classes de configuração de produção!


Especificando o Perfil Ativo nos Testes de Integração (@ActiveProfiles)

Como informamos ao Spring Test Framework qual perfil deve ser ativado ao carregar o ApplicationContext do nosso teste de integração?

Basta anotarmos nossa classe de teste com @ActiveProfiles(Profiles.INTEGRATION_TEST):

 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
package com.testwithspring.intermediate.message

import com.github.springtestdbunit.DbUnitTestExecutionListener
import com.github.springtestdbunit.annotation.DatabaseSetup
import com.github.springtestdbunit.annotation.DbUnitConfiguration
import com.testwithspring.intermediate.config.Profiles
import org.junit.jupiter.api.Tag
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.TestExecutionListeners
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener
import org.springframework.test.context.transaction.TransactionalTestExecutionListener
import org.springframework.test.context.web.ServletTestExecutionListener

@SpringBootTest(classes = [IntegrationTestContext::class])
@AutoConfigureMockMvc
@TestExecutionListeners(
    value = [
        DependencyInjectionTestExecutionListener::class,
        TransactionalTestExecutionListener::class,
        DbUnitTestExecutionListener::class,
        ServletTestExecutionListener::class
    ]
)
@DatabaseSetup("messages.xml")
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@Tag("integrationTest")
@ActiveProfiles(Profiles.INTEGRATION_TEST)
class GetMessageTest {
}

A anotação @ActiveProfiles funciona tanto em aplicações Spring Framework puras quanto em aplicações Spring Boot!


Resumo

Esta aula nos ensinou quatro coisas fundamentais:

  • É uma excelente prática substituir componentes externos ou não-determinísticos (como relógio do sistema Clock ou clientes HTTP externos) por implementações controladas nos testes de integração.
  • Podemos carregar arquivos de propriedades diferentes por ambiente usando @Profile + @PropertySource no Spring Framework, ou usando a convenção automática application-{profile}.properties no Spring Boot.
  • Podemos registrar beans diferentes para produção e para testes de integração anotando métodos @Bean ou classes @Configuration com @Profile (ou utilizando @TestConfiguration).
  • Ativamos o perfil desejado nos nossos testes de integração anotando a classe de teste com @ActiveProfiles.