Experimente o Elasticsearch na prática: Mergulhe em nossos notebooks de amostra, inicie um teste gratuito na nuvem ou experimente o Elastic em sua máquina local agora mesmo.
Neste artigo, apresentaremos e explicaremos duas maneiras de testar software usando o Elasticsearch como uma dependência externa do sistema. Vamos abordar testes usando mocks, bem como testes de integração, mostrar algumas diferenças práticas entre eles e dar algumas dicas sobre o que fazer com cada estilo.
Bons testes para avaliar a confiabilidade do sistema.
Um bom teste é aquele que aumenta a confiança de todas as pessoas envolvidas no processo de criação e manutenção de um sistema de TI. Os testes não devem ser pensados para serem modernos, rápidos ou para aumentar artificialmente a cobertura de código. Os testes desempenham um papel vital para garantir que:
- O que queremos entregar é algo que funcione na produção.
- O sistema satisfaz os requisitos e os contratos.
- Não haverá regressões no futuro.
- Os desenvolvedores (e outros membros da equipe envolvidos) estão confiantes de que o que criaram funcionará.
É claro que isso não significa que os testes não possam ser interessantes, rápidos ou aumentar a cobertura de código. Quanto mais rápido pudermos executar nosso conjunto de testes, melhor. A questão é que, na busca por reduzir a duração total do conjunto de testes, não devemos sacrificar a confiabilidade, a facilidade de manutenção e a segurança que os testes automatizados nos proporcionam.
Bons testes automatizados aumentam a confiança dos membros da equipe:
- Desenvolvedores: eles conseguem confirmar que o que estão fazendo funciona (mesmo antes que o código em que trabalham saia de suas máquinas).
- Equipe de garantia da qualidade: eles têm menos coisas para testar manualmente.
- Os operadores de sistemas e os SREs (Engenheiros de Confiabilidade de Site) estão mais tranquilos, pois os sistemas são mais fáceis de implantar e manter.
Por último, mas não menos importante: a arquitetura de um sistema. Adoramos quando os sistemas são organizados, fáceis de manter e a arquitetura é limpa e cumpre sua função. No entanto, às vezes podemos nos deparar com uma arquitetura que sacrifica demais em nome da desculpa conhecida como "assim é mais fácil de testar". Não há nada de errado em ser altamente testável – o problema surge quando o sistema é escrito principalmente para ser testável, em vez de atender às necessidades que justificam sua existência. É nesse momento que vemos a cauda abanando o cachorro.
Existem dois tipos de testes: simulações (mocks) e testes de dependência.
Existem muitas maneiras de visualizar os testes e, portanto, classificá-los. Neste post, vou me concentrar em apenas um aspecto da divisão dos testes: usar mocks (ou stubs, ou fakes, ou ...) versus usar dependências reais. No nosso caso, a dependência é o Elasticsearch.
Os testes que utilizam mocks são muito rápidos porque não precisam iniciar nenhuma dependência externa e tudo acontece apenas na memória. Em testes automatizados, o "mocking" consiste na utilização de objetos falsos em vez de objetos reais para testar partes de um programa sem usar as dependências reais. É por isso que são necessários e por isso que se destacam em qualquer teste de rede de detecção rápida, por exemplo. Validação de entrada. Não é necessário iniciar um banco de dados e fazer uma chamada a ele apenas para verificar se números negativos em uma solicitação não são permitidos, por exemplo.
No entanto, a introdução de simulações tem várias implicações:
- Nem tudo e em todas as situações pode ser facilmente simulado, portanto, as simulações têm impacto na arquitetura do sistema (o que às vezes é ótimo, outras vezes nem tanto).
- Os testes executados em mocks podem ser rápidos, mas o desenvolvimento desses testes pode levar bastante tempo, pois os mocks que refletem fielmente os sistemas que imitam geralmente não são fornecidos gratuitamente. Quem conhece o funcionamento do sistema precisa escrever os mocks da maneira correta, e esse conhecimento pode vir da experiência prática, do estudo da documentação e assim por diante.
- É necessário manter os simulados. Quando seu sistema depende de uma dependência externa e você precisa atualizar essa dependência, alguém precisa garantir que os mocks que a reproduzem também sejam atualizados com todas as alterações: alterações que quebram a compatibilidade, alterações documentadas e alterações não documentadas (que também podem ter impacto em nosso sistema). Isso se torna especialmente problemático quando você deseja atualizar uma dependência, mas seu conjunto de testes (que usa apenas mocks) não consegue garantir que todos os casos testados funcionarão corretamente.
- É preciso disciplina para garantir que o esforço seja direcionado para o desenvolvimento e teste do sistema, e não para simulações.
Por essas razões, muitas pessoas defendem seguir exatamente na direção oposta: nunca usar mocks (ou stubs, etc.), mas confiar exclusivamente em dependências reais. Essa abordagem funciona muito bem em demonstrações ou quando o sistema é pequeno e possui apenas alguns casos de teste que geram uma cobertura enorme. Esses testes podem ser testes de integração (grosso modo: verificar uma parte de um sistema em relação a algumas dependências reais) ou testes de ponta a ponta (usando todas as dependências reais ao mesmo tempo e verificando o comportamento do sistema em todas as suas extremidades, enquanto se reproduzem fluxos de trabalho do usuário que definem o sistema como utilizável e bem-sucedido). Uma clara vantagem de usar essa abordagem é que também verificamos (muitas vezes sem intenção) nossas suposições sobre as dependências e como as integramos ao sistema em que estamos trabalhando.
No entanto, quando os testes utilizam apenas dependências reais, precisamos considerar os seguintes aspectos:
- Alguns cenários de teste não precisam da dependência real (por exemplo, para verificar as invariantes estáticas de uma solicitação).
- Normalmente, esses testes não são executados em conjuntos completos nas máquinas dos desenvolvedores, porque esperar por feedback levaria muito tempo.
- Elas exigem mais recursos nas máquinas de CI e pode levar mais tempo para ajustar tudo e evitar desperdício de tempo e recursos.
- Pode não ser trivial inicializar dependências com dados de teste.
- Testes com dependências reais são ótimos para isolar o código antes de grandes refatorações, migrações ou atualizações de dependências.
- É mais provável que sejam testes opacos, ou seja, que não entrem em detalhes sobre o funcionamento interno do sistema em teste, mas que se preocupem com os resultados.
O ponto ideal: use ambos os testes.
Em vez de testar seu sistema com apenas um tipo de teste, você pode usar ambos os tipos quando fizer sentido e tentar melhorar o uso de ambos.
- Execute primeiro os testes baseados em mocks, pois são muito mais rápidos, e somente depois que todos forem bem-sucedidos, execute os testes de dependência, que são mais lentos.
- Escolha mocks para cenários onde dependências externas não são realmente necessárias: quando a criação de mocks levaria muito tempo, o código deve ser alterado drasticamente apenas para isso; dependa de dependências externas.
- Não há nada de errado em testar um trecho de código usando ambas as abordagens, desde que faça sentido.
Exemplo de SistemaEmTeste
Nas próximas seções, usaremos um exemplo que pode ser encontrado aqui. Trata-se de uma pequena aplicação de demonstração escrita em Java 21, utilizando o Maven como ferramenta de compilação, dependendo do cliente Elasticsearch e utilizando a mais recente adição ao Elasticsearch, o ES|QL (a nova linguagem de consulta procedural da Elastic). Se Java não é a sua linguagem de programação, você ainda poderá entender os conceitos que discutiremos a seguir e adaptá-los à sua pilha de tecnologias. O simples fato de usar um exemplo de código real facilita a explicação de certas coisas.
O BookSearcher nos ajuda a lidar com a pesquisa e a analisar dados, que no nosso caso são livros (como demonstrado em uma das postagens anteriores).
- Ele requer o Elasticsearch exatamente na versão
8.15.xcomo sua única dependência (vejaisCompatibleWithBackend()), por exemplo, porque não temos certeza se nosso código é compatível com versões futuras e temos certeza de que não é compatível com versões anteriores. Antes de atualizar o Elasticsearch em produção para uma versão mais recente, primeiro o atualizaremos nos testes para garantir que o comportamento do Sistema em Teste permaneça o mesmo. - Podemos usá-lo para pesquisar o número de livros publicados em um determinado ano (ver
numberOfBooksPublishedInYear). - Também podemos usá-lo quando precisamos analisar nosso conjunto de dados e descobrir os 20 autores mais publicados entre dois anos determinados (ver
mostPublishedAuthorsInYears).
Faça testes com simulações para começar.
Para criar os mocks usados em nossos testes, vamos usar o Mockito, uma biblioteca de mocks muito popular no ecossistema Java.
Podemos começar com o seguinte, para que os mocks sejam reinicializados antes de cada teste:
Como dissemos anteriormente, nem tudo pode ser facilmente testado usando mocks. Mas há coisas que podemos (e provavelmente até devemos) fazer. Vamos verificar se a única versão do Elasticsearch suportada é 8.15.x por enquanto (no futuro, poderemos ampliar o intervalo assim que confirmarmos que nosso sistema é compatível com versões futuras):
Podemos verificar de forma semelhante (simplesmente retornando uma versão secundária diferente) que nosso BookSearcher ainda não funcionará com 8.16.x , porque não temos certeza se será compatível com ele:
Agora vejamos como podemos alcançar algo semelhante ao testar com um Elasticsearch real. Para isso, vamos usar o módulo Elasticsearch da Testcontainers, que tem apenas um requisito: precisa de acesso ao Docker, pois executa contêineres Docker para você. De certo ponto de vista, o Testcontainers é simplesmente uma forma de operar contêineres Docker, mas em vez de fazer isso no seu Docker Desktop (ou similar), na sua CLI ou em scripts, você pode expressar suas necessidades na linguagem de programação que você conhece. Isso possibilita buscar imagens, iniciar contêineres, coletar o lixo após os testes, copiar arquivos de um lado para o outro, executar comandos, examinar logs, etc., diretamente do seu código de teste.
O esboço pode ter esta aparência:
Neste exemplo, contamos com a integração do Testcontainers com o JUnit , @Testcontainers e @Container, o que significa que não precisamos nos preocupar em iniciar o Elasticsearch antes de nossos testes e pará-lo depois. A única coisa que precisamos fazer é criar o cliente antes de cada teste e fechá-lo após cada teste (para evitar vazamentos de recursos, que poderiam afetar conjuntos de testes maiores).
Anotar um campo não estático com @Container significa que um novo contêiner será iniciado para cada teste, portanto não precisamos nos preocupar com dados desatualizados ou com a reinicialização do estado do contêiner. No entanto, em muitos testes, essa abordagem pode não apresentar um bom desempenho, por isso vamos compará-la com alternativas em uma das próximas publicações.
Observação:
Ao utilizar odocker.elastic.co(repositório oficial de imagens Docker da Elastic), você evita atingir seus limites no Docker Hub.
Recomenda-se também usar a mesma versão da sua dependência nos ambientes de teste e de produção, para garantir a máxima compatibilidade. Recomendamos também que você seja preciso ao selecionar a versão, por esse motivo, não há taglatestpara imagens do Elasticsearch.
Conectando-se ao Elasticsearch em testes
O cliente Java do Elasticsearch é capaz de se conectar ao Elasticsearch em execução em um contêiner de teste, mesmo com segurança e SSL/TLS habilitados (que são padrão nas versões 8.x, por isso não precisamos especificar nada relacionado à segurança na declaração do contêiner). Partindo do pressuposto que o Elasticsearch que você está usando em produção também tenha TLS e alguns recursos de segurança habilitados, recomenda-se configurar os testes de integração o mais próximo possível do cenário de produção e, portanto, não desabilitá-los nos testes.
Como obter os dados necessários para a conexão, assumindo que o contêiner está atribuído ao campo ou variável elasticsearch:
elasticsearch.getHost()irá fornecer o host no qual o contêiner está sendo executado (que na maioria das vezes provavelmente será"localhost", mas, por favor, não codifique isso diretamente, pois às vezes, dependendo da sua configuração, pode ser outro nome; portanto, o host deve sempre ser obtido dinamicamente).elasticsearch.getMappedPort(9200)irá fornecer a porta do host que você precisa usar para se conectar ao Elasticsearch em execução dentro do contêiner (porque cada vez que você inicia o contêiner, a porta externa é diferente, então esta também precisa ser uma chamada dinâmica).- A menos que tenham sido sobrescritos, o nome de usuário e a senha padrão são
"elastic"e"changeme", respectivamente. - Caso nenhum certificado SSL/TLS tenha sido especificado durante a configuração do contêiner e a conectividade segura não esteja desativada (que é o comportamento padrão a partir das versões 8.x), um certificado autoassinado será gerado. Confiar nisso (por exemplo) como o cURL pode fazer) o certificado pode ser obtido usando
elasticsearch.caCertAsBytes()(que retornaOptional<byte[]>), ou outra maneira conveniente é obterSSLContextusandocreateSslContextFromCa().
O resultado geral pode ser semelhante a este:
Outro exemplo de criação de uma instância de ElasticsearchClient pode ser encontrado no projeto de demonstração.
Observação:
Para criar um cliente em ambientes de produção, consulte a documentação.
Primeiro teste de integração
Nosso primeiro teste, para verificar se podemos criar BookSearcher usando o Elasticsearch versão 8.15.x, pode ser algo como:
Como podem ver, não precisamos configurar mais nada. Não precisamos simular a versão retornada pelo Elasticsearch, a única coisa que precisamos fazer é fornecer BookSearcher com um cliente conectado a uma instância real do Elasticsearch, que foi iniciada para nós pelo Testcontainers.
Os testes de integração se preocupam menos com os detalhes internos.
Vamos fazer uma pequena experiência: vamos supor que temos que parar de extrair dados do conjunto de resultados usando índices de coluna, e sim usar os nomes das colunas. Então, no método isCompatibleWithBackend em vez de
Teremos:
Ao executarmos novamente os dois testes, notaremos que o teste de integração com o Elasticsearch real ainda é aprovado sem problemas. No entanto, os testes usando mocks pararam de funcionar, porque simulamos chamadas como rs.getInt(int), não rs.getInt(String). Para que sejam aprovados, agora precisamos ou simulá-los, ou simulá-los ambos, dependendo de outros casos de uso que temos em nosso conjunto de testes.
Testes de integração podem ser como um canhão para matar uma mosca.
Os testes de integração são capazes de verificar o comportamento do sistema, mesmo que não haja necessidade de dependências externas. No entanto, usá-los dessa forma geralmente resulta em desperdício de tempo e recursos de execução. Vamos analisar o método mostPublishedAuthorsInYears(int minYear, int maxYear). As duas primeiras linhas são as seguintes:
A primeira declaração verifica uma condição que não depende do Elasticsearch (ou de qualquer outra dependência externa) de forma alguma. Portanto, não precisamos iniciar nenhum contêiner apenas para verificar se minYear é maior que maxYear, uma exceção é lançada.
Um teste de simulação simples, que também seja rápido e não consuma muitos recursos, é mais do que suficiente para garantir isso. Após configurar os mocks, podemos simplesmente prosseguir para:
Iniciar uma dependência, em vez de usar um mock, seria um desperdício neste caso de teste, pois não há chance de fazer uma chamada significativa para essa dependência.
No entanto, para verificar o comportamento a partir de String query = ..., que a consulta está escrita corretamente, os resultados são os esperados: a biblioteca cliente é capaz de enviar solicitações e respostas adequadas, não há alterações de sintaxe e, portanto, é muito mais fácil usar um teste de integração, por exemplo:
Dessa forma, podemos ter certeza de que, ao enviar nossos dados para o Elasticsearch (nesta ou em qualquer versão futura para a qual optarmos por migrar), nossa consulta nos dará exatamente o que esperamos: o formato dos dados não mudou, a consulta ainda é válida e todo o middleware (clientes, drivers, segurança etc.) continuará funcionando. Não precisamos nos preocupar em manter os mocks atualizados; a única alteração necessária é garantir a compatibilidade com, por exemplo, 8.15 alteraria isto:
O mesmo acontece se você decidir, por exemplo, Use a boa e velha QueryDSL em vez de ES|QL: os resultados que você receber da consulta (independentemente da linguagem) ainda serão os mesmos.
Utilize ambas as abordagens quando necessário.
O caso do método mostPublishedAuthorsInYears ilustra que um único método pode ser testado usando ambos os métodos. E talvez até devesse ser.
- Usar apenas mocks significa que temos que manter o mock e não temos nenhuma confiança ao atualizar nosso sistema.
- Utilizar apenas testes de integração significaria um desperdício de muitos recursos, sem necessidade.
Vamos recapitular
- É possível usar tanto testes de simulação (mocking) quanto testes de integração com o Elasticsearch.
- Use testes de mocking como fast-detection-net e somente se eles passarem com sucesso, inicie os testes com dependências (por exemplo, usando
./mvnw test '-Dtest=!TestInt*' && ./mvnw test '-Dtest=TestInt*'ou os plugins Failsafe e Surefire ). - Use mocks ao testar o comportamento do seu sistema ("linhas de código") onde a integração com dependências externas não é realmente importante (ou pode até ser ignorada).
- Utilize testes de integração para verificar suas suposições sobre a integração com sistemas externos.
- Não tenha receio de testar ambas as abordagens – se fizer sentido – de acordo com os pontos acima.
Poderíamos observar que ser tão rigoroso quanto à versão (no nosso caso, 8.15.x) é excessivo. Usar apenas a tag de versão poderia ser suficiente, mas lembre-se de que, neste post, ela representa todos os outros recursos que podem mudar entre as versões.
Na próxima parte desta série, veremos maneiras de inicializar o Elasticsearch em um contêiner de teste, com conjuntos de dados de teste. Informe-nos se você construiu algo com base neste blog ou se tiver dúvidas em nossos fóruns de discussão e no canal da comunidade no Slack.

