O terceiro artigo da série e uma pequena ramificação da série principal - desta vez, mostrarei como a Spring Integration Testing Library funciona e como funciona, o que acontece quando o teste é iniciado e como você pode ajustar o aplicativo e seu ambiente para o teste.
Fui solicitado a escrever este artigo por um comentário do Hixon10 sobre como usar uma base real, como o Postgres, em um teste de integração. O autor do comentário sugeriu o uso da conveniente biblioteca com todos os inclusos Embedded-Database-Spring-Test . E eu já adicionei um parágrafo e um exemplo de uso no código, mas depois pensei sobre isso. Obviamente, usar uma biblioteca pronta é correto e bom, mas se o objetivo é entender como escrever testes para um aplicativo Spring, será mais útil mostrar como implementar a mesma funcionalidade. Primeiro, esse é um ótimo motivo para falar sobre o que está por trás do Teste da Primavera . E segundo, acredito que você não pode confiar em bibliotecas de terceiros; se você não entender como elas estão organizadas, isso só levará ao fortalecimento do mito da "mágica" da tecnologia.
Desta vez, não haverá recurso para o usuário, mas haverá um problema que precisa ser resolvido - desejo iniciar o banco de dados real em uma porta aleatória e conectar o aplicativo a esse banco de dados temporário automaticamente e, após os testes, paro e excluo o banco de dados.
A princípio, como já é habitual, um pouco de teoria. Para pessoas que não estão muito familiarizadas com os conceitos de bin, contexto, configuração, recomendo atualizar o conhecimento, por exemplo, no meu artigo O verso do Spring / Habr .
Teste de mola
O Spring Test é uma das bibliotecas incluídas no Spring Framework, na verdade tudo o que é descrito na seção de documentação sobre testes de integração é exatamente isso. As quatro tarefas principais que a biblioteca resolve são:
- Gerenciar contêineres Spring IoC e seu cache entre testes
- Fornecer injeção de dependência para classes de teste
- Fornecer gerenciamento de transações adequado para testes de integração
- Forneça um conjunto de classes base para ajudar o desenvolvedor a escrever testes de integração
Eu recomendo a leitura da documentação oficial, ela diz muitas coisas úteis e interessantes. Aqui vou dar um breve aperto e algumas dicas práticas que são úteis para se ter em mente.
Teste o ciclo de vida

O ciclo de vida de um teste é assim:
- A extensão da estrutura de teste (
SpringRunner
para JUnit 4 e SpringExtension
para JUnit 5) chama o Bootstrapper de contexto de teste - O Boostrapper cria o
TestContext
- a classe principal que armazena o estado atual do teste e do aplicativo TestContext
configura ganchos diferentes (como iniciar transações antes do teste e reverter depois), injeta dependências nas classes de teste (todos os campos @Autowired
nas classes de teste) e cria contextos- Um contexto é criado usando o Context Loader - ele pega a configuração básica do aplicativo e a mescla com a configuração de teste (propriedades sobrepostas, perfis, compartimentos, inicializadores etc.)
- O contexto é armazenado em cache usando uma chave composta que descreve completamente o aplicativo - um conjunto de posições, propriedades, etc.
- Execuções de teste
Todo o trabalho sujo de gerenciar os testes é feito, de fato, por spring-test
, e o Spring Boot Test
por sua vez, adiciona várias classes auxiliares, como os familiares @DataJpaTest
e @SpringBootTest
, utilitários úteis como TestPropertyValues
para alterar dinamicamente as propriedades do contexto. Também permite que você execute o aplicativo como um servidor Web real ou como um ambiente simulado (sem acesso via HTTP), é conveniente limpar os componentes do sistema usando @MockBean
, etc.
Cache de Contexto
Talvez um dos tópicos muito obscuros do teste de integração que levante muitas questões e conceitos errôneos seja o cache de contexto (veja o parágrafo 5 acima) entre os testes e seu efeito na velocidade dos testes. Um comentário frequente que ouço é que os testes de integração são "lentos" e "executam o aplicativo para cada teste". Então, eles correm - mas não para todos os testes. Cada contexto (ou seja, instância do aplicativo) será reutilizado ao máximo, ou seja se 10 testes usarem a mesma configuração de aplicativo, o aplicativo iniciará uma vez para todos os 10 testes. O que significa a "mesma configuração" do aplicativo? Para o Spring Test, isso significa que o conjunto de beans, classes de configuração, perfis, propriedades etc. não foi alterado. Na prática, isso significa que, por exemplo, esses dois testes usarão o mesmo contexto:
@SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class FirstTest { } @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class SecondTest { }
O número de contextos no cache é limitado a 32 - além disso, de acordo com o princípio LRSU, um deles será excluído do cache.
O que pode impedir que o Spring Test reutilize o contexto do cache e crie um novo?
@DirtiesContext
A opção mais fácil é se o teste estiver marcado com anotações, o contexto não será armazenado em cache. Isso pode ser útil se o teste alterar o estado do aplicativo e você desejar "redefini-lo".
@MockBean
Uma opção muito óbvia, eu até a renderizei separadamente - o @MockBean substitui o bean real no contexto por um mock que pode ser testado através do Mockito (nos artigos a seguir, mostrarei como usá-lo). O ponto principal é que esta anotação altera o conjunto de beans no aplicativo e força o Spring Test a criar um novo contexto. Se usarmos o exemplo anterior, por exemplo, dois contextos já serão criados aqui:
@SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class FirstTest { } @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class SecondTest { @MockBean CakeFinder cakeFinderMock; }
@TestPropertySource
Qualquer alteração de propriedade altera automaticamente a chave de cache e um novo contexto é criado.
@ActiveProfiles
A alteração de perfis ativos também afetará o cache.
@ContextConfiguration
E, claro, qualquer alteração na configuração também criará um novo contexto.
Começamos a base
Agora, com todo esse conhecimento, tentaremos decolar Entenda como e onde você pode executar o banco de dados. Não há uma resposta correta aqui, depende dos requisitos, mas você pode pensar em duas opções:
- Execute uma vez antes de todos os testes da classe.
- Execute uma instância aleatória e um banco de dados separado para cada contexto em cache (potencialmente mais de uma classe).
Dependendo dos requisitos, você pode escolher qualquer opção. Se, no meu caso, o Postgres iniciar relativamente rápido e a segunda opção parecer adequada, a primeira poderá ser adequada para algo mais difícil.
A primeira opção não está ligada ao Spring, mas a uma estrutura de teste. Por exemplo, você pode fazer sua extensão para o JUnit 5 .
Se você reunir todo o conhecimento sobre a biblioteca de teste, os contextos e o cache, a tarefa se resumirá ao seguinte: ao criar um novo contexto de aplicativo, você precisará executar o banco de dados em uma porta aleatória e transferir os dados de conexão para o contexto .
ApplicationContextInitializer
interface ApplicationContextInitializer
é responsável por executar ações com o contexto antes de iniciar no Spring.
ApplicationContextInitializer
A interface possui apenas um método de initialize
, que é executado antes que o contexto seja "iniciado" (ou seja, antes que o método de refresh
seja chamado) e permite que você faça alterações no contexto - adicione posições, propriedades.
No meu caso, a classe fica assim:
public class EmbeddedPostgresInitializer implements ApplicationContextInitializer<GenericApplicationContext> { @Override public void initialize(GenericApplicationContext applicationContext) { EmbeddedPostgres postgres = new EmbeddedPostgres(); try { String url = postgres.start(); TestPropertyValues values = TestPropertyValues.of( "spring.test.database.replace=none", "spring.datasource.url=" + url, "spring.datasource.driver-class-name=org.postgresql.Driver", "spring.jpa.hibernate.ddl-auto=create"); values.applyTo(applicationContext); applicationContext.registerBean(EmbeddedPostgres.class, () -> postgres, beanDefinition -> beanDefinition.setDestroyMethodName("stop")); } catch (IOException e) { throw new RuntimeException(e); } } }
A primeira coisa que acontece aqui é que o Postgres incorporado é iniciado a partir da biblioteca yandex-qatools / postgresql-embedded . Em seguida, um conjunto de propriedades é criado - a URL JDBC para a base recém-lançada, o tipo de driver e o comportamento do Hibernate para o esquema (criado automaticamente). Uma coisa não óbvia é apenas spring.test.database.replace=none
- é o que dizemos ao DataJpaTest que não precisamos tentar conectar-se ao banco de dados incorporado, como H2, e não precisamos substituir a lixeira do DataSource (isso funciona).
E outro ponto importante é application.registerBean(…)
. Em geral, é claro que esse bean não pode ser registrado - se ninguém o usa no aplicativo, ele não é particularmente necessário. O registro é necessário apenas para especificar o método de destruição que o Spring chamará quando o contexto for destruído e, no meu caso, esse método chamará postgres.stop()
e interromperá o banco de dados.
Em geral, é tudo, a mágica terminou, se houver. Agora vou registrar esse inicializador em um contexto de teste:
@DataJpaTest @ContextConfiguration(initializers = EmbeddedPostgresInitializer.class) ...
Ou mesmo por conveniência, você pode criar sua própria anotação, porque todos gostamos de anotações!
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @DataJpaTest @ContextConfiguration(initializers = EmbeddedPostgresInitializer.class) public @interface EmbeddedPostgresTest { }
Agora, qualquer teste anotado por @EmbeddedPostgrestTest
iniciará o banco de dados em uma porta aleatória e, com um nome aleatório, configure o Spring para conectar-se a esse banco de dados e pare-o no final do teste.
@EmbeddedPostgresTest class JpaCakeFinderTestWithEmbeddedPostgres { ... }
Conclusão
Eu queria mostrar que não há mágica misteriosa no Spring, existem muitos mecanismos internos "inteligentes" e flexíveis, mas sabendo que você pode obter controle total sobre os testes e o próprio aplicativo. Em geral, em projetos de combate, não motivo todos a escreverem seus próprios métodos e classes para configurar o ambiente de integração para testes; se houver uma solução pronta, você poderá executá-la. Embora se o método inteiro tiver 5 linhas de código, provavelmente será desnecessário arrastar a dependência para o projeto, especialmente não entender a implementação.
Links para outros artigos da série