Aplicativos TDD no Spring Boot: ajustando os testes e trabalhando com o contexto

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:


  1. A extensão da estrutura de teste ( SpringRunner para JUnit 4 e SpringExtension para JUnit 5) chama o Bootstrapper de contexto de teste
  2. O Boostrapper cria o TestContext - a classe principal que armazena o estado atual do teste e do aplicativo
  3. 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
  4. 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.)
  5. O contexto é armazenado em cache usando uma chave composta que descreve completamente o aplicativo - um conjunto de posições, propriedades, etc.
  6. 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:


  1. Execute uma vez antes de todos os testes da classe.
  2. 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


Source: https://habr.com/ru/post/pt446184/


All Articles