True-to-production integration testing
Testcontainers is a Java library that facilitates the creation of integration tests that closely resemble production environments. It enables developers to test their applications within the same container used in production, thus reducing discrepancies between development and deployment settings.
By using the Testcontainers framework, developers can analyze their applications externally, without accessing internal application details. This approach ensures that integration tests accurately evaluate various test classes and components. Although integration tests require more setup and configuration time compared to unit tests, they provide a comprehensive assessment of the application’s behavior in a realistic environment.
Unit tests focus on testing individual modules or components of an application and are quicker to run. Due to shorter development cycles and time constraints, developers often prioritize running unit tests. However, with Testcontainers, developers can efficiently write and run integration tests that closely resemble real-world production scenarios.
Development and production parity
Development and production parity is one of the factors in the twelve-factor app, which is a methodology to build modern applications. The idea behind development and production parity is to keep the development, staging, and production environments similar, regarding time, personnel, and tools. To simplify the development process, developers often use tools in development that are different from production. For example, you might use a local Maven build to build a project for your application in development, but the application might be deployed to a container in production. Differences between the environments can cause a test to fail in production, though the test passes in the development environment. Testcontainers helps achieve development and production parity by testing the application in an environment similar to production.
Writing integration tests with the Testcontainers framework
Testcontainers is a Java library that enables you to thoroughly test your MicroProfile and Jakarta EE applications in a testing environment that closely resembles your production setup.
In the following example, Testcontainers is used to start a Docker container that is running an Open Liberty server with a test application deployed. It uses REST Assured to send HTTP requests to the service. This test case adds a person, retrieves all people, and verifies the correctness of the data.
@Testcontainers
public class ServiceTest {
// Latest liberty image
static final DockerImageName libertyImage = DockerImageName.parse("open-liberty:23.0.0.4-full-java11-openj9");
// Create container and copy application + server.xml
@Container
static final GenericContainer<?> liberty = new GenericContainer<>(libertyImage)
.withExposedPorts(9080, 9443)
.withCopyFileToContainer(MountableFile.forHostPath("build/libs/testapp.war"), "/config/dropins/testapp.war")
.withCopyFileToContainer(MountableFile.forHostPath("build/resources/main/liberty/config/server.xml"), "/config/server.xml")
.waitingFor(Wait.forLogMessage(".*CWWKZ0001I: Application .* started in .* seconds.*", 1))
.withLogConsumer(new LogConsumer(ServiceTest.class, "liberty"));
// Setup RestAssured to query our service
static RequestSpecification requestSpecification;
@BeforeAll
static void getServiceURL() {
String baseUri = "http://" + liberty.getHost() + ":" + liberty.getMappedPort(9080) + "/testapp/people";
requestSpecification = new RequestSpecBuilder()
.setBaseUri(baseUri)
.build();
}
// Run our test
@Test
public void testAddPerson() {
// Add new person to service
given(requestSpecification)
.header("Content-Type", "application/json")
.queryParam("name", "bob")
.queryParam("age", "24")
.when()
.post("/")
.then()
.statusCode(200);
// Get and verify only one person exists
List<Long> allIDs = given(requestSpecification)
.accept("application/json")
.when()
.get("/")
.then()
.statusCode(200)
.extract().body()
.jsonPath().getList("id");
assertEquals(1, allIDs.size());
// Verify person is bob
String actual = given(requestSpecification)
.accept("application/json")
.when()
.get("/" + allIDs.get(0))
.then()
.statusCode(200)
.extract().body()
.jsonPath().getString("name");
assertEquals("bob", actual);
}
Test Elements
The @Testcontainers
annotation is a custom Junit5 extension that controls the lifecycle of the Docker container (create, run, stop, and delete) within the scope of this test class.
The ServiceTest
class starts with a static field libertyImage
, which represents the Docker image to be used for the test.
The @Container
annotation marks the field that constructs the Docker container by using the libertyImage
image. It makes ports 9080
and 9443
accessible and copies the application and server configuration files into the container. It waits for a log message that indicates that the application started successfully and assigns a log consumer for logging container logs.
The @BeforeAll
annotation marks the method that creates the base URL for the service is constructed by using the host and mapped port of the liberty container. The request specification is created with the base URL.
The @Test
annotation marks the method that runs a series of tests on the application service.
Test Logic
Inside the test method, a new person is added to the service by sending a POST request with JSON payload that contains the name and age parameters.
Upon adding the person, a GET request is sent to retrieve all people from the service. The response is validated to ensure a successful status code (200)
. The IDs of all the people are extracted from the response by using JSONPath.
The number of IDs is checked to ensure that only one person exists in the service.
Another GET
request is sent to retrieve the details of the person with the extracted ID. The response is validated to ensure a successful status code (200)
. The name of the person is extracted from the response by using JSONPath.
Finally, the extracted name is compared with the expected value bob
using the assertEquals()
method.