paint-brush
Tudo o que você precisa saber sobre o banco de testes de carga baseado em Testcontainerspor@avvero
223 leituras

Tudo o que você precisa saber sobre o banco de testes de carga baseado em Testcontainers

por Anton Belyaev15m2024/06/07
Read on Terminal Reader

Muito longo; Para ler

O objetivo deste artigo é demonstrar uma abordagem para criar uma configuração para testes de carga da mesma forma que os testes de integração regulares são escritos: na forma de testes Spock usando Testcontainers em um ambiente de projeto Gradle. Utilitários de teste de carga como Gatling, WRK e Yandex.Tank são usados.
featured image - Tudo o que você precisa saber sobre o banco de testes de carga baseado em Testcontainers
Anton Belyaev HackerNoon profile picture

O uso de Testcontainers melhorou radicalmente o processo de trabalho com cenários de teste. Graças a esta ferramenta, criar ambientes para testes de integração ficou mais simples (veja o artigo Isolamento em Testes com Kafka ). Agora podemos lançar facilmente contêineres com diferentes versões de bancos de dados, corretores de mensagens e outros serviços. Para testes de integração, Testcontainers provou ser indispensável.


Embora o teste de carga seja menos comum que o teste funcional, ele pode ser muito mais agradável. Estudar gráficos e analisar o desempenho de um determinado serviço pode trazer um verdadeiro prazer. Essas tarefas são raras, mas são especialmente emocionantes para mim.


O objetivo deste artigo é demonstrar uma abordagem para criar uma configuração para testes de carga da mesma forma que os testes de integração regulares são escritos: na forma de testes Spock usando Testcontainers em um ambiente de projeto Gradle. Utilitários de teste de carga como Gatling, WRK e Yandex.Tank são usados.

Criando um ambiente de teste de carga

Conjunto de ferramentas: Gradle + Spock Framework + Testcontainers. A variante de implementação é um módulo Gradle separado. Os utilitários de teste de carga usados são Gatling, WRK e Yandex.Tank.


Existem duas abordagens para trabalhar com o objeto de teste:

  • Testando imagens publicadas;
  • Construindo imagens a partir do código fonte do projeto e testes.


No primeiro caso, temos um conjunto de testes de carga independentes da versão e alterações do projeto. Esta abordagem é mais fácil de manter no futuro, mas está limitada a testar apenas imagens publicadas. Podemos, é claro, construir manualmente essas imagens localmente, mas isso é menos automatizado e reduz a reprodutibilidade. Ao executar em CI/CD sem as imagens necessárias, os testes falharão.


No segundo caso, os testes são executados na versão mais recente do serviço. Isso permite integrar testes de carga no CI e obter alterações nos dados de desempenho entre versões de serviço. No entanto, os testes de carga geralmente demoram mais que os testes de unidade. A decisão de incluir esses testes no CI como parte do portão de qualidade depende de você.


Este artigo considera a primeira abordagem. Graças ao Spock, podemos executar testes em múltiplas versões do serviço para análise comparativa:

 where: image | _ 'avvero/sandbox:1.0.0' | _ 'avvero/sandbox:1.1.0' | _

É importante observar que o objetivo deste artigo é demonstrar a organização do espaço de teste, e não o teste de carga em grande escala.

Serviço alvo

Para o objeto de teste, vamos usar um serviço HTTP simples chamado Sandbox, que publica um endpoint e usa dados de uma fonte externa para lidar com solicitações. O serviço possui um banco de dados.

O código fonte do serviço, incluindo o Dockerfile, está disponível no repositório do projeto spring-sandbox .

Visão geral da estrutura do módulo

À medida que nos aprofundamos nos detalhes posteriormente neste artigo, quero começar com uma breve visão geral da estrutura do módulo Gradle de load-tests para fornecer uma compreensão de sua composição:

 load-tests/ |-- src/ | |-- gatling/ | | |-- scala/ | | | |-- MainSimulation.scala # Main Gatling simulation file | | |-- resources/ | | | |-- gatling.conf # Gatling configuration file | | | |-- logback-test.xml # Logback configuration for testing | |-- test/ | | |-- groovy/ | | | |-- pw.avvero.spring.sandbox/ | | | | |-- GatlingTests.groovy # Gatling load test file | | | | |-- WrkTests.groovy # Wrk load test file | | | | |-- YandexTankTests.groovy # Yandex.Tank load test file | | |-- java/ | | | |-- pw.avvero.spring.sandbox/ | | | | |-- FileHeadLogConsumer.java # Helper class for logging to a file | | |-- resources/ | | | |-- wiremock/ | | | | |-- mappings/ # WireMock setup for mocking external services | | | | | |-- health.json | | | | | |-- forecast.json | | | |-- yandex-tank/ # Yandex.Tank load testing configuration | | | | |-- ammo.txt | | | | |-- load.yaml | | | | |-- make_ammo.py | | | |-- wrk/ # LuaJIT scripts for Wrk | | | | |-- scripts/ | | | | | |-- getForecast.lua |-- build.gradle

Repositório do projeto - https://github.com/avvero/testing-bench .

Ambiente

Pela descrição acima, vemos que o serviço possui duas dependências: o serviço https://external-weather-api.com e um banco de dados. Sua descrição será fornecida abaixo, mas vamos começar habilitando todos os componentes do esquema para se comunicarem em um ambiente Docker — descreveremos a rede:

 def network = Network.newNetwork()

e forneça aliases de rede para cada componente. Isto é extremamente conveniente e nos permite descrever estaticamente os parâmetros de integração.

Dependências como WireMock e os próprios utilitários de teste de carga requerem configuração para funcionar. Podem ser parâmetros que podem ser passados para o contêiner ou arquivos e diretórios inteiros que precisam ser montados nos contêineres.


Além disso, precisamos recuperar os resultados do seu trabalho nos contêineres. Para resolver essas tarefas, precisamos fornecer dois conjuntos de diretórios:


  • workingDirectory — o diretório de recursos do módulo, diretamente em load-tests/ .


  • reportDirectory — o diretório para os resultados do trabalho, incluindo métricas e logs. Mais sobre isso estará na seção de relatórios.

Base de dados

O serviço Sandbox usa Postgres como banco de dados. Vamos descrever essa dependência da seguinte maneira:

 def postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withNetwork(network) .withNetworkAliases("postgres") .withUsername("sandbox") .withPassword("sandbox") .withDatabaseName("sandbox")


A declaração especifica o alias de rede postgres , que o serviço Sandbox usará para se conectar ao banco de dados. Para completar a descrição da integração com o banco de dados, o serviço precisa ser fornecido com os seguintes parâmetros:

 'spring.datasource.url' : 'jdbc:postgresql://postgres:5432/sandbox', 'spring.datasource.username' : 'sandbox', 'spring.datasource.password' : 'sandbox', 'spring.jpa.properties.hibernate.default_schema': 'sandbox'


A estrutura do banco de dados é gerenciada pelo próprio aplicativo usando Flyway, portanto, nenhuma manipulação adicional do banco de dados é necessária no teste.

Solicitações simuladas para https://external-weather-api.com

Se não tivermos a possibilidade, a necessidade ou o desejo de executar o componente real em um contêiner, podemos fornecer uma simulação para sua API. Para o serviço https://external-weather-api.com, é usado WireMock.


A declaração do contêiner WireMock ficará assim:

 def wiremock = new GenericContainer<>("wiremock/wiremock:3.5.4") .withNetwork(network) .withNetworkAliases("wiremock") .withFileSystemBind("${workingDirectory}/src/test/resources/wiremock/mappings", "/home/wiremock/mappings", READ_WRITE) .withCommand("--no-request-journal") .waitingFor(new LogMessageWaitStrategy().withRegEx(".*https://wiremock.io/cloud.*")) wiremock.start()


WireMock requer configuração simulada. A instrução withFileSystemBind descreve a ligação do sistema de arquivos entre o caminho do arquivo local e o caminho dentro do contêiner Docker. Neste caso, o diretório "${workingDirectory}/src/test/resources/wiremock/mappings" na máquina local será montado em /home/wiremock/mappings dentro do contêiner WireMock.


Abaixo está uma parte adicional da estrutura do projeto para entender a composição do arquivo no diretório:

 load-tests/ |-- src/ | |-- test/ | | |-- resources/ | | | |-- wiremock/ | | | | |-- mappings/ | | | | | |-- health.json | | | | | |-- forecast.json


Para garantir que os arquivos de configuração simulados sejam carregados e aceitos corretamente pelo WireMock, você pode usar um contêiner auxiliar:

 helper.execInContainer("wget", "-O", "-", "http://wiremock:8080/health").getStdout() == "Ok"


O contêiner auxiliar é descrito a seguir:

 def helper = new GenericContainer<>("alpine:3.17") .withNetwork(network) .withCommand("top")


A propósito, o IntelliJ IDEA versão 2024.1 introduziu suporte para WireMock e o IDE fornece sugestões ao formar arquivos de configuração simulados.

Configuração de inicialização do serviço de destino

A declaração do contêiner de serviço Sandbox é a seguinte:

 def javaOpts = ' -Xloggc:/tmp/gc/gc.log -XX:+PrintGCDetails' + ' -XX:+UnlockDiagnosticVMOptions' + ' -XX:+FlightRecorder' + ' -XX:StartFlightRecording:settings=default,dumponexit=true,disk=true,duration=60s,filename=/tmp/jfr/flight.jfr' def sandbox = new GenericContainer<>(image) .withNetwork(network) .withNetworkAliases("sandbox") .withFileSystemBind("${reportDirectory}/logs", "/tmp/gc", READ_WRITE) .withFileSystemBind("${reportDirectory}/jfr", "/tmp/jfr", READ_WRITE) .withEnv([ 'JAVA_OPTS' : javaOpts, 'app.weather.url' : 'http://wiremock:8080', 'spring.datasource.url' : 'jdbc:postgresql://postgres:5432/sandbox', 'spring.datasource.username' : 'sandbox', 'spring.datasource.password' : 'sandbox', 'spring.jpa.properties.hibernate.default_schema': 'sandbox' ]) .waitingFor(new LogMessageWaitStrategy().withRegEx(".*Started SandboxApplication.*")) .withStartupTimeout(Duration.ofSeconds(10)) sandbox.start()

Parâmetros notáveis e configurações de JVM incluem:

  • Coleta de informações do evento de coleta de lixo.
  • Uso do Java Flight Recorder (JFR) para registrar dados de desempenho da JVM.


Além disso, são configurados diretórios para salvar os resultados de diagnóstico do serviço.

Exploração madeireira

Se você precisar ver os logs de qualquer contêiner em um arquivo, o que provavelmente será necessário durante o estágio de gravação e configuração do cenário de teste, você poderá usar as seguintes instruções ao descrever o contêiner:

 .withLogConsumer(new FileHeadLogConsumer("${reportDirectory}/logs/${alias}.log"))


Neste caso, é utilizada a classe FileHeadLogConsumer , que permite gravar uma quantidade limitada de logs em um arquivo. Isto é feito porque o registo inteiro provavelmente não é necessário em cenários de teste de carga, e um registo parcial será suficiente para avaliar se o serviço está a funcionar corretamente.

Implementação de testes de carga

Existem muitas ferramentas para teste de carga. Neste artigo, proponho considerar o uso de três deles: Gatling, Wrk e Yandex.Tank. Todas as três ferramentas podem ser usadas independentemente umas das outras.

Gatling

Gatling é uma ferramenta de teste de carga de código aberto escrita em Scala. Permite a criação de cenários de testes complexos e fornece relatórios detalhados. O arquivo de simulação principal do Gatling é conectado como um recurso Scala ao módulo, tornando-o conveniente para trabalhar usando toda a gama de suporte do IntelliJ IDEA, incluindo destaque de sintaxe e navegação por métodos para referência de documentação.


A configuração do contêiner para Gatling é a seguinte:

 def gatling = new GenericContainer<>("denvazh/gatling:3.2.1") .withNetwork(network) .withFileSystemBind("${reportDirectory}/gatling-results", "/opt/gatling/results", READ_WRITE) .withFileSystemBind("${workingDirectory}/src/gatling/scala", "/opt/gatling/user-files/simulations", READ_WRITE) .withFileSystemBind("${workingDirectory}/src/gatling/resources", "/opt/gatling/conf", READ_WRITE) .withEnv("SERVICE_URL", "http://sandbox:8080") .withCommand("-s", "MainSimulation") .waitingFor(new LogMessageWaitStrategy() .withRegEx(".*Please open the following file: /opt/gatling/results.*") .withStartupTimeout(Duration.ofSeconds(60L * 2)) ); gatling.start()

A configuração é quase idêntica a outros contêineres:

  • Monte o diretório para relatórios de reportDirectory .
  • Monte o diretório para arquivos de configuração workingDirectory .
  • Monte o diretório para arquivos de simulação workingDirectory .


Além disso, os parâmetros são passados para o contêiner:

  • A variável de ambiente SERVICE_URL com o valor de URL do serviço Sandbox. Embora, como mencionado anteriormente, o uso de aliases de rede permita codificar a URL diretamente no código do cenário.


  • O comando -s MainSimulation para executar uma simulação específica.


Aqui está um lembrete da estrutura do arquivo fonte do projeto para entender o que está sendo passado e onde:

 load-tests/ |-- src/ | |-- gatling/ | | |-- scala/ | | | |-- MainSimulation.scala # Main Gatling simulation file | | |-- resources/ | | | |-- gatling.conf # Gatling configuration file | | | |-- logback-test.xml # Logback configuration for testing

Como este é o contêiner final e esperamos obter resultados após sua conclusão, definimos a expectativa .withRegEx(".*Please open the following file: /opt/gatling/results.*") . O teste terminará quando esta mensagem aparecer nos logs do contêiner ou após 60 * 2 segundos.


Não vou me aprofundar no DSL dos cenários desta ferramenta. Você pode conferir o código do cenário utilizado no repositório do projeto .

Trabalho

Wrk é uma ferramenta de teste de carga simples e rápida. Pode gerar uma carga significativa com recursos mínimos. Os principais recursos incluem:

  • Suporte para scripts Lua para configurar solicitações.
  • Alto desempenho devido ao multithreading.
  • Facilidade de uso com dependências mínimas.


A configuração do contêiner para Wrk é a seguinte:

 def wrk = new GenericContainer<>("ruslanys/wrk") .withNetwork(network) .withFileSystemBind("${workingDirectory}/src/test/resources/wrk/scripts", "/tmp/scripts", READ_WRITE) .withCommand("-t10", "-c10", "-d60s", "--latency", "-s", "/tmp/scripts/getForecast.lua", "http://sandbox:8080/weather/getForecast") .waitingFor(new LogMessageWaitStrategy() .withRegEx(".*Transfer/sec.*") .withStartupTimeout(Duration.ofSeconds(60L * 2)) ) wrk.start()


Para fazer o Wrk funcionar com solicitações ao serviço Sandbox, é necessária a descrição da solicitação por meio de um script Lua, portanto montamos o diretório do script a partir workingDirectory . Usando o comando, executamos o Wrk, especificando o script e a URL do método de serviço de destino. Wrk grava um relatório no log com base em seus resultados, que pode ser usado para definir expectativas.

Yandex.Tanque

Yandex.Tank é uma ferramenta de teste de carga desenvolvida pela Yandex. Ele oferece suporte a vários mecanismos de teste de carga, como JMeter e Phantom. Para armazenar e exibir resultados de testes de carga, você pode usar o serviço gratuito Overload .


Aqui está a configuração do contêiner:

 copyFiles("${workingDirectory}/src/test/resources/yandex-tank", "${reportDirectory}/yandex-tank") def tank = new GenericContainer<>("yandex/yandex-tank") .withNetwork(network) .withFileSystemBind("${reportDirectory}/yandex-tank", "/var/loadtest", READ_WRITE) .waitingFor(new LogMessageWaitStrategy() .withRegEx(".*Phantom done its work.*") .withStartupTimeout(Duration.ofSeconds(60L * 2)) ) tank.start()


A configuração do teste de carga do Sandbox é representada por dois arquivos: load.yaml e ammo.txt . Como parte da descrição do contêiner, os arquivos de configuração são copiados para reportDirectory , que será montado como o diretório de trabalho. Aqui está a estrutura dos arquivos fonte do projeto para entender o que está sendo passado e onde:

 load-tests/ |-- src/ | |-- test/ | | |-- resources/ | | | |-- yandex-tank/ | | | | |-- ammo.txt | | | | |-- load.yaml | | | | |-- make_ammo.py

Relatórios

Os resultados do teste, incluindo registros e registros de desempenho da JVM, são salvos no diretório build/${timestamp} , onde ${timestamp} representa o carimbo de data/hora de cada execução de teste.


Os seguintes relatórios estarão disponíveis para revisão:

  • Registros do coletor de lixo.
  • Registros do WireMock.
  • Logs de serviço de destino.
  • Registros de trabalho.
  • JFR (gravação de voo Java).


Se Gatling foi usado:

  • Relatório Gatling.
  • Registros Gatling.


Se Wrk foi usado:

  • Registros de trabalho.


Se Yandex.Tank foi usado:

  • Arquivos de resultados Yandex.Tank, com upload adicional para Overload .
  • Registros Yandex.Tank.


A estrutura de diretórios dos relatórios é a seguinte:

 load-tests/ |-- build/ | |-- ${timestamp}/ | | |-- gatling-results/ | | |-- jfr/ | | |-- yandex-tank/ | | |-- logs/ | | | |-- sandbox.log | | | |-- gatling.log | | | |-- gc.log | | | |-- wiremock.log | | | |-- wrk.log | | | |-- yandex-tank.log | |-- ${timestamp}/ | |-- ...

Conclusão

O teste de carga é uma fase crucial no ciclo de vida de desenvolvimento de software. Ajuda a avaliar o desempenho e a estabilidade de uma aplicação sob diversas condições de carga. Este artigo apresentou uma abordagem para criar um ambiente de teste de carga usando Testcontainers, que permite uma configuração fácil e eficiente do ambiente de teste.


Testcontainers simplificam significativamente a criação de ambientes para testes de integração, proporcionando flexibilidade e isolamento. Para testes de carga, esta ferramenta permite a implantação dos containers necessários com diferentes versões de serviços e bancos de dados, facilitando a realização de testes e melhorando a reprodutibilidade dos resultados.


Os exemplos de configuração fornecidos para Gatling, Wrk e Yandex.Tank, juntamente com a configuração do contêiner, demonstram como integrar efetivamente várias ferramentas e gerenciar parâmetros de teste.


Além disso, foi descrito o processo de registro e salvamento dos resultados dos testes, essencial para analisar e melhorar o desempenho da aplicação. Esta abordagem pode ser expandida no futuro para suportar cenários mais complexos e integração com outras ferramentas de monitorização e análise.


Obrigado por sua atenção a este artigo e boa sorte em seu esforço para escrever testes úteis!