Testes de Integração para Repositórios Spring Data JPA
Ao concluir esta lição, você será capaz de:
- Compreender que tipo de dataset do DbUnit (ou script SQL) devemos usar ao inicializar nosso banco de dados em um estado conhecido para testes de repositório.
- Escrever testes de integração limpos e confiáveis para repositórios gerenciados pelo Spring Data JPA.
Esta lição assume que:
1. Introdução ao Sistema Sob Teste (SUT)
Durante esta lição, escreveremos testes de integração para um método de repositório customizado que busca tarefas no banco de dados. Esse método se chama search() e recebe o termo de busca (searchTerm) como parâmetro. A implementação do método search() cumpre os seguintes requisitos de negócio:
- Deve retornar as informações das tarefas cujo
title(título) oudescription(descrição) contenha o termo de busca informado. - Deve ordenar as tarefas retornadas em ordem crescente pelo
titleda tarefa. - A busca deve ser case-insensitive (ignorar maiúsculas e minúsculas).
O código-fonte da interface que declara nossos métodos customizados de repositório em Kotlin é o seguinte:
|
|
Como funcionam repositórios customizados no Spring Data JPA:
Na prática, escreveremos testes de integração injetando a interface principal
TaskRepository(que estendeJpaRepository<Task, Long>eCustomTaskRepository). Como implementamos nossa camada de persistência com Spring Data JPA e queríamos fornecer uma consulta otimizada (via JPQL, Criteria API, Querydsl ouJdbcClient) para esses dois métodos de leitura retornando projeções DTO diretamente, nós os declaramos na interface fragmentoCustomTaskRepository(cuja implementaçãoTaskRepositoryImplé automaticamente detectada pelo Spring Data).
A classe TaskListDTO contém o resumo de uma única tarefa retornada pelo método search(). Seu código-fonte em Kotlin é o seguinte:
|
|
A seguir, precisamos inicializar nosso banco de dados em um estado conhecido antes da execução dos testes de integração. Vamos ver como planejar esse dataset.
2. Inicializando o Banco de Dados em um Estado Conhecido
Como aprendemos nas lições de boas práticas de preparação de dados, devemos utilizar datasets que sejam o menor possível. Além disso, como estamos escrevendo testes de integração para um método de consulta (search()) que lê informações do banco de dados, precisamos garantir que esse único dataset nos permita testar todos os casos de uso necessários.
Isso significa que devemos inicializar nosso banco de dados criando um dataset que insira duas linhas na tabela tasks. Inserimos exatamente duas linhas na tabela tasks por duas razões:
- Se quisermos garantir que as tarefas sejam retornadas na ordem correta quando mais de um registro corresponder à busca, precisamos de pelo menos duas linhas na tabela
tasks. - Não precisamos inserir nenhuma linha nas outras tabelas do banco (como
tagsoutasks_tags) porque a implementação do métodosearch()consulta exclusivamente os dados da tabelatasks(e as chaves estrangeiras opcionais comoassignee_idecloser_idnão exigem joins na listagem resumida).
O nome do nosso arquivo de dataset é tasks.xml, e seu conteúdo é o seguinte:
|
|
Após criarmos nosso dataset tasks.xml, devemos adicionar as constantes correspondentes na nossa classe de constantes de teste (Tasks) para manter o código de teste sincronizado com o dataset. Declaramos as constantes seguindo estes dois passos:
- Declarar as constantes que contêm os termos de busca utilizados nos testes de integração do repositório (
SEARCH_TERM_ONE_MATCHcom letras maiúsculas/minúsculas misturadas"Esso"para validar o case-insensitivity em"lesson", eSEARCH_TERM_NOT_FOUND). - Declarar as constantes que contêm as informações das tarefas presentes no banco de dados. Como o método
search()retorna apenasid,statusetitledas tarefas encontradas, podemos focar apenas nessas propriedades por enquanto.
Após declararmos as constantes necessárias, o código-fonte do objeto Tasks em Kotlin ficará assim:
|
|
Nota:
O objeto
Taskscompleto do projeto declara outras constantes para os demais campos das tarefas (DESCRIPTION,CREATION_TIME, etc.). Como essas constantes adicionais não são necessárias nesta aula específica de repositório, nós as omitimos aqui e as adicionaremos na próxima aula, quando testarmos os controllers que exibem todos os detalhes da tarefa.
Agora, vamos configurar a nossa classe de teste de integração.
3. Configurando a Nossa Classe de Teste de Integração
Antes de escrevermos os testes de integração para o nosso método customizado de repositório, precisamos adicionar a configuração específica do teste na classe. Fazemos isso seguindo estes dois passos:
- Configurar o dataset do DbUnit que insere os dados de teste no banco de dados. Como o método
search()apenas lê informações do banco (sem alterá-lo), todos os métodos de teste podem compartilhar o mesmo dataset anotando a classe com@DatabaseSetup("/com/testwithspring/intermediate/tasks.xml"). - Injetar o objeto
TaskRepositorytestado na nossa classe de teste usando@Autowired.
Após configurarmos nossa classe de teste, seu código-fonte ficará assim:
|
|
Nota: Não detalhamos novamente cada anotação de configuração da classe porque já cobrimos toda a arquitetura de configuração no Tópico 03.
Com a classe configurada, podemos finalmente escrever os testes de integração para o nosso repositório!
4. Escrevendo os Testes de Integração para o Repositório
Vamos escrever testes de integração para o método search() da interface TaskRepository. Começaremos verificando que o método funciona conforme o esperado quando nenhuma tarefa é encontrada.
Cenário 1: Nenhuma Tarefa Encontrada
Para garantir que o método testado funcione corretamente quando nenhuma tarefa corresponder ao termo pesquisado, escrevemos um teste de integração que verifica se repository.search(Tasks.SEARCH_TERM_NOT_FOUND) retorna uma lista vazia (isEmpty()).
Após escrevermos esse teste de integração, o código-fonte da nossa classe SearchTasksWhenNoTasksIsFoundTest ficará assim:
|
|
A seguir, verificaremos que o método search() funciona conforme o esperado quando uma única tarefa é encontrada.
Cenário 2: Uma Tarefa Encontrada
Para verificar que o método testado funciona corretamente quando exatamente uma tarefa corresponde ao termo de busca ("Esso"), devemos escrever dois testes de integração focados:
- Verificar que o método
search()retorna uma lista contendo exatamente 1 tarefa (hasSize(1)). - Garantir que o método
search()retorna os dados da tarefa correta (Tasks.WriteLesson).
Após escrevermos esses dois testes de integração, o código-fonte da nossa classe SearchTasksWhenOneTaskIsFoundTest ficará assim:
|
|
Dica com
@Nestedno JUnit 6:Como veremos nas aulas avançadas de JUnit 6, você também pode agrupar
WhenNoTasksIsFoundeWhenOneTaskIsFound(além de um terceiro cenárioWhenTwoTasksAreFoundbuscando por"Write"para validar a ordenação ascendente pelo título!) como classes@Nested inner classdentro de uma única classeTaskRepositorySearchIntegrationTest, compartilhando a mesma configuração de contexto e dataset no topo.
Vamos resumir o que aprendemos nesta lição.
Resumo
Esta lição nos ensinou três pontos essenciais:
- Ao escrever testes de integração para um método que consulta informações no banco de dados, devemos criar um dataset pequeno e focado que nos permita exercitar todos os casos de uso necessários (busca vazia, correspondência única, correspondência múltipla ordenada).
- Quando o método testado apenas lê informações do banco de dados, todos os métodos de teste podem reutilizar o mesmo dataset configurado no nível da classe.
- Obtemos a instância real do repositório testado injetando-a diretamente na nossa classe de teste via
@Autowired.