Utilizing Testcontainers has radically improved the method of working with check situations. Because of this device, creating environments for integration exams has develop into less complicated (see the article Isolation in Testing with Kafka). Now we are able to simply launch containers with completely different variations of databases, message brokers, and different providers. For integration exams, Testcontainers has confirmed indispensable. Though load testing is much less frequent than practical testing, it may be far more satisfying. Finding out graphs and analyzing the efficiency of a specific service can deliver actual pleasure. Such duties are uncommon, however they’re particularly thrilling for me.
The aim of this text is to display an strategy to making a setup for load testing in the identical means that common integration exams are written: within the type of Spock exams utilizing Testcontainers in a Gradle challenge atmosphere. Load-testing utilities resembling Gatling, WRK, and Yandex.Tank are used.
Making a Load Testing Setting
Toolset: Gradle + Spock Framework + Testcontainers. The implementation variant is a separate Gradle module. Load testing utilities used are Gatling, WRK, and Yandex.Tank.
There are two approaches to working with the check object:
- Testing revealed pictures;
- Constructing pictures from the challenge’s supply code and testing.
Within the first case, we’ve got a set of load exams which might be impartial of the challenge’s model and modifications. This strategy is simpler to keep up sooner or later, however it’s restricted to testing solely revealed pictures. We are able to, in fact, manually construct these pictures domestically, however that is much less automated and reduces reproducibility. When operating in CI/CD with out the required pictures, the exams will fail.
Within the second case, the exams are run on the newest model of the service. This permits for integrating load exams into CI and acquiring efficiency information modifications between service variations. Nonetheless, load exams normally take longer than unit exams. The choice to incorporate such exams in CI as a part of the standard gate is as much as you.
This text considers the primary strategy. Because of Spock, we are able to run exams on a number of variations of the service for comparative evaluation:
the place:
picture | _
'avvero/sandbox:1.0.0' | _
'avvero/sandbox:1.1.0' | _
You will need to observe that the objective of this text is to display the group of the testing area, not full-scale load testing.
Goal Service
For the testing object, let’s take a easy HTTP service named Sandbox, which publishes an endpoint and makes use of information from an exterior supply to deal with requests. The service has a database.
The supply code of the service, together with the Dockerfile, is out there within the challenge repository spring-sandbox.
Module Construction Overview
As we delve into the small print later within the article, I wish to begin with a short overview of the construction of the load-tests Gradle module to offer an understanding of its composition:
load-tests/
|-- src/
| |-- gatling/
| | |-- scala/
| | | |-- MainSimulation.scala # Most important Gatling simulation file
| | |-- assets/
| | | |-- gatling.conf # Gatling configuration file
| | | |-- logback-test.xml # Logback configuration for testing
| |-- check/
| | |-- groovy/
| | | |-- pw.avvero.spring.sandbox/
| | | | |-- GatlingTests.groovy # Gatling load check file
| | | | |-- WrkTests.groovy # Wrk load check file
| | | | |-- YandexTankTests.groovy # Yandex.Tank load check file
| | |-- java/
| | | |-- pw.avvero.spring.sandbox/
| | | | |-- FileHeadLogConsumer.java # Helper class for logging to a file
| | |-- assets/
| | | |-- wiremock/
| | | | |-- mappings/ # WireMock setup for mocking exterior providers
| | | | | |-- well being.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
|-- construct.gradle
Challenge repository.
Setting
From the outline above, we see that the service has two dependencies: the service https://external-weather-api.com and a database. Their description will likely be supplied beneath, however let’s begin by enabling all elements of the scheme to speak in a Docker atmosphere — we’ll describe the community:
def community = Community.newNetwork()
And supply community aliases for every part. That is extraordinarily handy and permits us to statically describe the mixing parameters.
Dependencies resembling WireMock and the load testing utilities themselves require configuration to work. These might be parameters that may be handed to the container or complete recordsdata and directories that have to be mounted to the containers. As well as, we have to retrieve the outcomes of their work from the containers. To resolve these duties, we have to present two units of directories:
workingDirectory
— the module’s useful resource listing, straight atload-tests/
.reportDirectory
— the listing for the outcomes of the work, together with metrics and logs. Extra on this will likely be within the part on experiences.
Database
The Sandbox service makes use of Postgres as its database. Let’s describe this dependency as follows:
def postgres = new PostgreSQLContainer("postgres:15-alpine")
.withNetwork(community)
.withNetworkAliases("postgres")
.withUsername("sandbox")
.withPassword("sandbox")
.withDatabaseName("sandbox")
The declaration specifies the community alias postgres, which the Sandbox service will use to connect with the database. To finish the mixing description with the database, the service must be supplied with the next parameters:
'spring.datasource.url' : 'jdbc:postgresql://postgres:5432/sandbox',
'spring.datasource.username' : 'sandbox',
'spring.datasource.password' : 'sandbox',
'spring.jpa.properties.hibernate.default_schema': 'sandbox'
The database construction is managed by the appliance itself utilizing Flyway, so no extra database manipulations are wanted within the check.
Mocking Requests to https://external-weather-api.com
If we do not have the likelihood, necessity, or need to run the precise part in a container, we are able to present a mock for its API. For the service https://external-weather-api.com, WireMock is used.
The declaration of the WireMock container will appear to be this:
def wiremock = new GenericContainer("wiremock/wiremock:3.5.4")
.withNetwork(community)
.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.begin()
WireMock requires mock configuration. The withFileSystemBind
instruction describes the file system binding between the native file path and the trail contained in the Docker container. On this case, the listing "${workingDirectory}/src/test/resources/wiremock/mappings"
on the native machine will likely be mounted to /house/wiremock/mappings
contained in the WireMock container. Beneath is an extra a part of the challenge construction to know the file composition within the listing:
load-tests/
|-- src/
| |-- check/
| | |-- assets/
| | | |-- wiremock/
| | | | |-- mappings/
| | | | | |-- well being.json
| | | | | |-- forecast.json
To make sure that the mock configuration recordsdata are appropriately loaded and accepted by WireMock, you should utilize a helper container:
helper.execInContainer("wget", "-O", "-", "http://wiremock:8080/health").getStdout() == "Ok"
The helper container is described as follows:
def helper = new GenericContainer("alpine:3.17")
.withNetwork(community)
.withCommand("top")
By the way in which, IntelliJ IDEA model 2024.1 launched help for WireMock, and the IDE gives ideas when forming mock configuration recordsdata.
Goal Service Launch Configuration
The declaration of the Sandbox service container appears as follows:
.withNetwork(community)
.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(Period.ofSeconds(10))
sandbox.begin()” data-lang=”text/x-java”>
def javaOpts=" -Xloggc:/tmp/gc/gc.log -XX:+PrintGCDetails" +
' -XX:+UnlockDiagnosticVMOptions' +
' -XX:+FlightRecorder' +
' -XX:StartFlightRecording:settings=default,dumponexit=true,disk=true,length=60s,filename=/tmp/jfr/flight.jfr'
def sandbox = new GenericContainer(picture)
.withNetwork(community)
.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(Period.ofSeconds(10))
sandbox.begin()
Notable parameters and JVM settings embody:
- Assortment of rubbish assortment occasion info.
- Use of Java Flight Recorder (JFR) to report JVM efficiency information.
Moreover, directories are configured for saving the diagnostic outcomes of the service.
Logging
If it’s essential to see the logs of any container to a file, which is probably going crucial in the course of the check situation writing and configuration stage, you should utilize the next directions when describing the container:
.withLogConsumer(new FileHeadLogConsumer("${reportDirectory}/logs/${alias}.log"))
On this case, the FileHeadLogConsumer
class is used, which permits writing a restricted quantity of logs to a file. That is achieved as a result of the whole log is probably going not wanted in load-testing situations, and a partial log will likely be adequate to evaluate whether or not the service is functioning appropriately.
Implementation of Load Checks
There are lots of instruments for load testing. On this article, I suggest to think about using three of them: Gatling, Wrk, and Yandex.Tank. All three instruments can be utilized independently of one another.
Gatling
Gatling is an open-source load-testing device written in Scala. It permits the creation of complicated testing situations and gives detailed experiences. The principle simulation file of Gatling is linked as a Scala useful resource to the module, making it handy to work with utilizing the total vary of help from IntelliJ IDEA, together with syntax highlighting and navigation via strategies for documentation reference.
The container configuration for Gatling is as follows:
def gatling = new GenericContainer("denvazh/gatling:3.2.1")
.withNetwork(community)
.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(Period.ofSeconds(60L * 2))
);
gatling.begin()
The setup is sort of similar to different containers:
- Mount the listing for experiences from
reportDirectory
. - Mount the listing for configuration recordsdata from
workingDirectory
. - Mount the listing for simulation recordsdata from
workingDirectory
.
Moreover, parameters are handed to the container:
- The
SERVICE_URL
atmosphere variable with the URL worth for the Sandbox service. Nonetheless, as talked about earlier, utilizing community aliases permits hardcoding the URL straight within the situation code. - The
-s MainSimulation
command to run a particular simulation.
Here’s a reminder of the challenge supply file construction to know what’s being handed and the place:
load-tests/
|-- src/
| |-- gatling/
| | |-- scala/
| | | |-- MainSimulation.scala # Most important Gatling simulation file
| | |-- assets/
| | | |-- gatling.conf # Gatling configuration file
| | | |-- logback-test.xml # Logback configuration for testing
Since that is the ultimate container, and we anticipate to get outcomes upon its completion, we set the expectation .withRegEx(".*Please open the following file: /opt/gatling/results.*")
. The check will finish when this message seems within the container logs or after `60 * 2`
seconds.
I cannot delve into the DSL of this device’s situations. You’ll be able to try the code of the used situation within the challenge repository.
Wrk
Wrk is an easy and quick load-testing device. It may possibly generate a big load with minimal assets. Key options embody:
- Help for Lua scripts to configure requests.
- Excessive efficiency on account of multithreading.
- Ease of use with minimal dependencies.
The container configuration for Wrk is as follows:
def wrk = new GenericContainer("ruslanys/wrk")
.withNetwork(community)
.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(Period.ofSeconds(60L * 2))
)
wrk.begin()
To make Wrk work with requests to the Sandbox service, the request description by way of a Lua script is required, so we mount the script listing from workingDirectory
. Utilizing the command, we run Wrk, specifying the script and the URL of the goal service technique. Wrk writes a report back to the log primarily based on its outcomes, which can be utilized to set expectations.
Yandex.Tank
Yandex.Tank is a load testing device developed by Yandex. It helps numerous load-testing engines, resembling JMeter and Phantom. For storing and displaying load check outcomes, you should utilize the free service Overload.
Right here is the container configuration:
copyFiles("${workingDirectory}/src/test/resources/yandex-tank", "${reportDirectory}/yandex-tank")
def tank = new GenericContainer("yandex/yandex-tank")
.withNetwork(community)
.withFileSystemBind("${reportDirectory}/yandex-tank", "/var/loadtest", READ_WRITE)
.waitingFor(new LogMessageWaitStrategy()
.withRegEx(".*Phantom done its work.*")
.withStartupTimeout(Period.ofSeconds(60L * 2))
)
tank.begin()
The load testing configuration for Sandbox is represented by two recordsdata: load.yaml
and ammo.txt
. As a part of the container description, configuration recordsdata are copied to the reportDirectory
, which will likely be mounted because the working listing. Right here is the construction of the challenge supply recordsdata to know what’s being handed and the place:
load-tests/
|-- src/
| |-- check/
| | |-- assets/
| | | |-- yandex-tank/
| | | | |-- ammo.txt
| | | | |-- load.yaml
| | | | |-- make_ammo.py
Experiences
Check outcomes, together with JVM efficiency recordings and logs, are saved within the listing construct/${timestamp}
, the place ${timestamp}
represents the timestamp of every check run.
The next experiences will likely be out there for overview:
- Rubbish Collector logs.
- WireMock logs.
- Goal service logs.
- Wrk logs.
- JFR (Java Flight Recording).
If Gatling was used:
- Gatling report.
- Gatling logs.
If Wrk was used:
If Yandex.Tank was used:
- Yandex.Tank end result recordsdata, with an extra add to [Overload](https://overload.yandex.internet/).
- Yandex.Tank logs.
The listing construction for the experiences is as follows:
load-tests/
|-- construct/
| |-- ${timestamp}/
| | |-- gatling-results/
| | |-- jfr/
| | |-- yandex-tank/
| | |-- logs/
| | | |-- sandbox.log
| | | |-- gatling.log
| | | |-- gc.log
| | | |-- wiremock.log
| | | |-- wrk.log
| | | |-- yandex-tank.log
| |-- ${timestamp}/
| |-- ...
Conclusion
Load testing is a vital part within the software program improvement lifecycle. It helps assess the efficiency and stability of an software beneath numerous load situations. This text introduced an strategy to making a load testing atmosphere utilizing Testcontainers, which permits for a simple and environment friendly setup of the testing atmosphere.
Testcontainers considerably simplify the creation of environments for integration exams, offering flexibility and isolation. For load testing, this device permits the deployment of crucial containers with completely different variations of providers and databases, making it simpler to conduct exams and enhance end result reproducibility.
The supplied configuration examples for Gatling, Wrk, and Yandex.Tank, together with container setup, demonstrates how one can successfully combine numerous instruments and handle testing parameters. Moreover, the method of logging and saving check outcomes was described, which is crucial for analyzing and enhancing software efficiency. This strategy might be expanded sooner or later to help extra complicated situations and integration with different monitoring and evaluation instruments.
Thanks in your consideration to this text, and good luck in your endeavor to jot down helpful exams!