Utilizing Testcontainers has radically improved the method of working with take a look at eventualities. Due to this instrument, creating environments for integration exams has develop into easier (see the article Isolation in Testing with Kafka). Now we are able to simply launch containers with totally different variations of databases, message brokers, and different companies. For integration exams, Testcontainers has confirmed indispensable. Though load testing is much less widespread than purposeful testing, it may be way more pleasurable. Learning graphs and analyzing the efficiency of a selected 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 venture atmosphere. Load-testing utilities akin to 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 take a look at object:
- Testing revealed photographs;
- Constructing photographs from the venture’s supply code and testing.
Within the first case, we’ve got a set of load exams which are unbiased of the venture’s model and adjustments. This strategy is simpler to keep up sooner or later, however it’s restricted to testing solely revealed photographs. We will, in fact, manually construct these photographs domestically, however that is much less automated and reduces reproducibility. When operating in CI/CD with out the required photographs, the exams will fail.
Within the second case, the exams are run on the most recent model of the service. This enables for integrating load exams into CI and acquiring efficiency knowledge adjustments 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. Due to 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' | _
It is very important notice that the aim of this text is to display the group of the testing house, 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 knowledge from an exterior supply to deal with requests. The service has a database.
The supply code of the service, together with the Dockerfile, is offered within the venture repository spring-sandbox.
Module Construction Overview
As we delve into the small print later within the article, I wish to begin with a quick overview of the construction of the load-tests Gradle module to supply an understanding of its composition:
load-tests/
|-- src/
| |-- gatling/
| | |-- scala/
| | | |-- MainSimulation.scala # Predominant Gatling simulation file
| | |-- sources/
| | | |-- gatling.conf # Gatling configuration file
| | | |-- logback-test.xml # Logback configuration for testing
| |-- take a look at/
| | |-- groovy/
| | | |-- pw.avvero.spring.sandbox/
| | | | |-- GatlingTests.groovy # Gatling load take a look at file
| | | | |-- WrkTests.groovy # Wrk load take a look at file
| | | | |-- YandexTankTests.groovy # Yandex.Tank load take a look at file
| | |-- java/
| | | |-- pw.avvero.spring.sandbox/
| | | | |-- FileHeadLogConsumer.java # Helper class for logging to a file
| | |-- sources/
| | | |-- wiremock/
| | | | |-- mappings/ # WireMock setup for mocking exterior companies
| | | | | |-- 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
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 element. That is extraordinarily handy and permits us to statically describe the mixing parameters.
Dependencies akin to WireMock and the load testing utilities themselves require configuration to work. These may be parameters that may be handed to the container or complete information 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, instantly 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 hook up 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 further database manipulations are wanted within the take a look at.
Mocking Requests to https://external-weather-api.com
If we do not have the likelihood, necessity, or want to run the precise element 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 like this:
def wiremock = new GenericContainer<>("wiremock/wiremock:3.5.4")
.withNetwork(community)
.withNetworkAliases("wiremock")
.withFileSystemBind("${workingDirectory}/src/take a look at/sources/wiremock/mappings", "/house/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/take a look at/sources/wiremock/mappings"
on the native machine will likely be mounted to /house/wiremock/mappings
contained in the WireMock container. Under is an extra a part of the venture construction to grasp the file composition within the listing:
load-tests/
|-- src/
| |-- take a look at/
| | |-- sources/
| | | |-- wiremock/
| | | | |-- mappings/
| | | | | |-- well being.json
| Â | Â | Â | Â | Â |-- forecast.json
To make sure that the mock configuration information are appropriately loaded and accepted by WireMock, you should utilize a helper container:
helper.execInContainer("wget", "-O", "-", "http://wiremock:8080/well being").getStdout() == "Okay"
The helper container is described as follows:
def helper = new GenericContainer<>("alpine:3.17")
.withNetwork(community)
    .withCommand("prime")
By the way in which, IntelliJ IDEA model 2024.1 introduced support for WireMock, and the IDE supplies strategies when forming mock configuration information.
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(“.*Began SandboxApplication.*”))
.withStartupTimeout(Period.ofSeconds(10))
sandbox.begin()” data-lang=”textual content/x-java”>
def javaOpts=" -Xloggc:/tmp/gc/gc.log -XX:+PrintGCDetails" +
' -XX:+UnlockDiagnosticVMOptions' +
' -XX:+FlightRecorder' +
' -XX:StartFlightRecording:settings=default,dumponexit=true,disk=true,period=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(".*Began SandboxApplication.*"))
.withStartupTimeout(Period.ofSeconds(10))
sandbox.begin()
Notable parameters and JVM settings embrace:
- Assortment of rubbish assortment occasion info.
- Use of Java Flight Recorder (JFR) to document JVM efficiency knowledge.
Moreover, directories are configured for saving the diagnostic outcomes of the service.
Logging
If that you must see the logs of any container to a file, which is probably going obligatory through the take a look at 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 executed as a result of the complete log is probably going not wanted in load-testing eventualities, and a partial log will likely be adequate to evaluate whether or not the service is functioning appropriately.
Implementation of Load Checks
There are numerous 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 instrument written in Scala. It permits the creation of advanced testing eventualities and supplies detailed experiences. The primary simulation file of Gatling is linked as a Scala useful resource to the module, making it handy to work with utilizing the complete 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", "/decide/gatling/outcomes", READ_WRITE)
.withFileSystemBind("${workingDirectory}/src/gatling/scala", "/decide/gatling/user-files/simulations", READ_WRITE)
.withFileSystemBind("${workingDirectory}/src/gatling/sources", "/decide/gatling/conf", READ_WRITE)
.withEnv("SERVICE_URL", "http://sandbox:8080")
.withCommand("-s", "MainSimulation")
.waitingFor(new LogMessageWaitStrategy()
.withRegEx(".*Please open the next file: /decide/gatling/outcomes.*")
.withStartupTimeout(Period.ofSeconds(60L * 2))
);
gatling.begin()
The setup is sort of an identical to different containers:
- Mount the listing for experiences from
reportDirectory
. - Mount the listing for configuration information from
workingDirectory
. - Mount the listing for simulation information 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 instantly within the situation code. - The
-s MainSimulation
command to run a selected simulation.
Here’s a reminder of the venture supply file construction to grasp what’s being handed and the place:
load-tests/
|-- src/
| |-- gatling/
| | |-- scala/
| | | |-- MainSimulation.scala # Predominant Gatling simulation file
| | |-- sources/
| | | |-- 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 next file: /decide/gatling/outcomes.*")
. The take a look at will finish when this message seems within the container logs or after `60 * 2`
seconds.
I cannot delve into the DSL of this instrument’s eventualities. You’ll be able to take a look at the code of the used situation within the project repository.
Wrk
Wrk is an easy and quick load-testing instrument. It may well generate a major load with minimal sources. Key options embrace:
- Help for Lua scripts to configure requests.
- Excessive efficiency because 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/take a look at/sources/wrk/scripts", "/tmp/scripts", READ_WRITE)
.withCommand("-t10", "-c10", "-d60s", "--latency", "-s", "/tmp/scripts/getForecast.lua", "http://sandbox:8080/climate/getForecast")
.waitingFor(new LogMessageWaitStrategy()
.withRegEx(".*Switch/sec.*")
.withStartupTimeout(Period.ofSeconds(60L * 2))
)
wrk.begin()
To make Wrk work with requests to the Sandbox service, the request description through 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 methodology. 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 instrument developed by Yandex. It helps numerous load-testing engines, akin to JMeter and Phantom. For storing and displaying load take a look at outcomes, you should utilize the free service Overload.
Right here is the container configuration:
copyFiles("${workingDirectory}/src/take a look at/sources/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 executed its work.*")
.withStartupTimeout(Period.ofSeconds(60L * 2))
)
tank.begin()
The load testing configuration for Sandbox is represented by two information: load.yaml
and ammo.txt
. As a part of the container description, configuration information are copied to the reportDirectory
, which will likely be mounted because the working listing. Right here is the construction of the venture supply information to grasp what’s being handed and the place:
load-tests/
|-- src/
| |-- take a look at/
| | |-- sources/
| | | |-- yandex-tank/
| | | | |-- ammo.txt
| | | | |-- load.yaml
| Â | Â | Â | Â |-- make_ammo.py
Experiences
Take a look at outcomes, together with JVM efficiency recordings and logs, are saved within the listing construct/${timestamp}
, the place ${timestamp}
represents the timestamp of every take a look at run.
The next experiences will likely be out there for assessment:
- 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 consequence information, with an extra add to [Overload](https://overload.yandex.web/).
- 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 section within the software program growth lifecycle. It helps assess the efficiency and stability of an utility beneath numerous load situations. This text introduced an strategy to making a load testing atmosphere utilizing Testcontainers, which permits for a straightforward 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 instrument permits the deployment of obligatory containers with totally different variations of companies and databases, making it simpler to conduct exams and enhance consequence reproducibility.
The supplied configuration examples for Gatling, Wrk, and Yandex.Tank, together with container setup, demonstrates how you can successfully combine numerous instruments and handle testing parameters. Moreover, the method of logging and saving take a look at outcomes was described, which is crucial for analyzing and bettering utility efficiency. This strategy may be expanded sooner or later to help extra advanced eventualities and integration with different monitoring and evaluation instruments.
Thanks on your consideration to this text, and good luck in your endeavor to put in writing helpful exams!