Our test class makes the following assumptions. The code under test represents a web service with endpoints for managing a To Do list. The test cases interact with those endpoints to verify the correct behavior by performing HTTP calls against them.
The TestContainers library provides a rich API for starting containers as test fixtures. The API includes functionality for building a Dockerfile, creating an image from the Dockerfile and starting a container for the image. The test code in listing 3 uses the JUnit 5-compatible annotations to achieve exactly that.
ToDoWebServiceFunctionalTest.java
@Testcontainers
public class ToDoWebServiceFunctionalTest {
private final static File DISTRIBUTION_DIR = new File(System.getProperty("distribution.dir"));
private final static String ARCHIVE_NAME = System.getProperty("archive.name");
@Container
private GenericContainer appContainer = createContainer();
private static GenericContainer createContainer() {
return new GenericContainer(buildImageDockerfile())
.withExposedPorts(8080)
.waitingFor(Wait.forHttp("/actuator/health")
.forStatusCode(200));
}
private static ImageFromDockerfile buildImageDockerfile() {
return new ImageFromDockerfile()
.withFileFromFile(ARCHIVE_NAME, new File(DISTRIBUTION_DIR, ARCHIVE_NAME))
.withDockerfileFromBuilder(builder -> builder
.from("openjdk:jre-alpine")
.copy(ARCHIVE_NAME, "/app/" + ARCHIVE_NAME)
.entryPoint("java", "-jar", "/app/" + ARCHIVE_NAME)
.build());
}
}
Listing 3. Creating and starting a container as test fixture
I want to point out of a couple of interesting pieces in the listing. The Dockerfile consists of only three instructions. It uses the base image named openjdk:jre-alpine
to make the resuting image as small as possible. Then it copies the JAR file and points the java
command to it as entrypoint. When starting the container, test execution blocks until the Spring Boot application becomes "healthy". In this case, the application uses Actuator to expose a health status endpoint.
Using the JAR file for running the application in a container may not be the most performant option. Every single change to the source code will result in the need to rebuild the archive.
The better alternative is to define the Dockerfile in a way that creates separate layers for external dependencies, resources files and class files. That way, Docker can cache unchanged layers. You will also want to disable automatic deletion of images for TestContainers.
It’s the test cases' responsibility to verify the application’s endpoints by making a HTTP call and inspecting the response. The GenericContainer
class exposes methods for retrieving the the container’s IP address and its exposed ports. With this information, you can determine the endpoint URL exposed by the container, as shown in listing 4.
ToDoWebServiceFunctionalTest.java
@Test
@DisplayName("can retrieve all items before and after inserting new ones")
void retrieveAllItems() {
// Use endpoint URL to make HTTP calls
}
private URL buildEndpointUrl(String context) {
StringBuilder url = new StringBuilder();
url.append("http://");
url.append(appContainer.getContainerIpAddress());
url.append(":");
url.append(appContainer.getFirstMappedPort());
url.append(context);
try {
return new URL(url.toString());
} catch (MalformedURLException e) {
throw new RuntimeException("Invalid URL", e);
}
}
Listing 4. Building the endpoint URL for the container
Executing the test from the build works just fine. But what about running tests in the IDE?