Testing Spring Boot: Docker with Testcontainers and JUnit 5. MySQL and other images.

logo spring

When approaching the design of integration and end-to-end tests, we have to decide what to do with the external systems that our application uses. The most typical case is databases. In addition, we must remember that creating test doubles objects defeats the purpose of this type of testing.

>> Read this post in Spanish here <<

Contents

  1. A big problem and a great solution
  2. Sample project
  3. Introducing Testcontainers for Java
    1. pom file
    2. The first test
    3. Running the test
    4. Initial script for MySQL
    5. Test datasets with @Sql
  4. Sharing a container between tests classes
  5. Reusing containers
  6. The quick setup for relational databases
  7. Faster databases
  8. Generic images
  9. Other features
    1. Dockerfile
    2. Docker compose
    3. Networking
  10. Source code

A big problem and a great solution

My introductory tutorial to automated testing in Spring Boot, currently only available in Spanish (*), explains the usage of an in-memory database as an alternative to MySQL using a sample project. With this strategy, we speed up the execution of the tests and give them more autonomy by reducing the dependencies on external systems.

(*) Check the annotations @AutoConfigureTestDatabase and @DataJpaTest.

Nevertheless, there is a significant problem with this solution. For the tests to be realistic and accurate, they must use an environment as close as possible to the final operating environment. For example, it is of little use to test the application’s code with H2 (an embedded database) if it uses MySQL. And that is assuming that we don’t have incompatible SQL statements between both databases.

Because of the above, in the aforementioned tutorial, most of the tests used a MySQL database exclusively for testing. The need to install and configure MySQL disappeared with Docker. Still, we must create and manage a suitable container in each environment (programmers’ machines, integration, testing…). No matter what we do, there is no entirely satisfactory solution…until we discover Testcontainers, the hero we need 😍

In this post, you will learn how to automate with Testcontainers the creation and execution of temporary Docker containers for your tests (JUnit 5 and Spring Boot). I will pay special attention to performance: you don’t want to slow down the tests, which are already slow due to their nature. Although I will focus on MySQL, at the end of the post I will explain how to use any Docker image with an example that, in addition, doesn’t require Spring Framework.

Sample project

The sample project is a Maven project based on Spring Boot 3.0, with support for accessing MySQL 8 with the JDBC API, that will contain some JUnit 5 tests (the Jupiter testing library). Therefore, in the pom file we need the jdbc and test starters, as well as the MySQL JDBC driver:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.7</version>
        <relativePath/>
    </parent>
    <groupId>com.danielme</groupId>
    <artifactId>spring-testcontainers</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-testcontainers</name>
    <description>Demo project for Spring Boot and TestContainers</description>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Note. Spring Boot 3.0 requires Java 17.

The class with the main method that bootstraps the project as a Spring Boot application:

package com.danielme.spring.testcontainers;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringTestcontainersApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringTestcontainersApplication.class, args);
	}

}

You can import the project into any IDE, such as Eclipse or IntelliJ. Also, in the project root folder you will find the Maven wrapper. This small tool lets you to use Maven without having it installed on your computer; just run any Maven command through the mvnw.cmd (Windows) or mvnw (Linux) script:

mvnw clean test

Introducing Testcontainers for Java

Testcontainers is an open-source library compatible with JUnit (4 and 5) and Spock. Its purpose is to manage Docker containers that you can easily integrate into your tests. Although it focuses on databases and browsers compatible with Selenium WebDriver, Testcontainers is valid for any "dockerizable" service. There are also versions for other languages, such as Python and .NET.

pom file

Testcontainers provides a dependency of type bom (bill of materials) that lets you to manage the version of all its libraries comfortably:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>testcontainers-bom</artifactId>
            <version>${testcontainers.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<properties>
    <testcontainers.version>1.17.3</testcontainers.version>
</properties>

Testcontainers with Jupiter support:

 <dependency>
     <groupId>org.testcontainers</groupId>
     <artifactId>junit-jupiter</artifactId>
     <scope>test</scope>
 </dependency>

You also need the MySQL module. We will see that it is optional, but it simplifies the job.

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mysql</artifactId>
    <scope>test</scope>
</dependency>
The first test

Let’s go down the rabbit hole! Here is the first test class with a minimal configuration:

@SpringBootTest
@Testcontainers
class MySQLContainerTest {

You have the usual @SpringBootTest annotation. It boots a Spring context integrated with JUnit 5 thanks to the SpringExtension extension.

The relevant part is that the class is marked with @Testcontainers. This annotation triggers the JUnit 5 extension that allows Testcontainers to launch the containers as part of the testing execution cycle.

Notice that Spring and Testcontainers don’t integrate with each other; instead, they both do it independently with JUnit 5. They collaborate harmoniously due to their JUnit 5 extensions.

Next, you declare each container you need as a field of the test class. If the field is static, the container it represents is shared by the tests of the class. Thus, the container will be started before the execution of the first test and will remain available until the last one is executed. At that time, it will be unmercifully destroyed.

The alternative is to use a non-static field. In that case, containers are created and destroyed for every test. Something that, except for unusual scenarios, you don’t want—it is a massive waste of time.

Whatever your choice, mark with @Container the fields that represent the containers. These fields are of the GenericContainer class, but many services have modules that provide specializations of GenericContainer that ease the configuration. That’s why we added the MySQL module to the pom.

The MySQL module, as depicted in the class diagram, contains the MySQLContainer public class. You instantiate this class by specifying the name and version of a Docker image with MySQL. Our sample project uses the official image for MySQL available on Docker Hub:

@Container
private static final MySQLContainer mySQLContainer = new MySQLContainer<>("mysql:8.0.30");

By default, MySQLContainer creates a database called "test" accessible with the credentials "test\test". If any of these values are wrong, change it:

@Container
private static final MySQLContainer mySQLContainer = new MySQLContainer<>("mysql:8.0.30")
        .withDatabaseName("testcontainer")
        .withUsername("test")
        .withPassword("test");

So far, so good. Spring Boot requires the connection parameters for the MySQL server running inside the container to create the DataSource bean for the tests. The testing setup must replace the values you already have in the application.properties file corresponding to the database used by the project. You can accomplish this by creating the file /src/test/resources/application.properties with the specific configuration values for the tests. Here is a typical configuration for MySQL:

spring.datasource.url=jdbc:mysql://localhost:3306/db_name
spring.datasource.username=user
spring.datasource.password=password

But there is a problem: Testcontainers exposes the ports published by the container on host random free ports. The goal is to ensure that running services don’t already use the chosen ports—two processes can’t listen on the same port.

Therefore, you have to figure out the port number and find a way to declare it in the Spring configuration. Since this number is variable, it isn’t feasible to define it in a configuration file or in the @SpringBootTest annotation by using the properties field.

You solve the first challenge by asking mySQLContainer for the port through which Testcontainers exposes the standard MySQL port:

mySQLContainer.getMappedPort(3306);

Again, everything is easier with MySQLContainer, as it gives you the full URL:

mySQLContainer.getJdbcUrl();

As for the Spring Boot configuration, create a static method marked with @DynamicPropertySource. The only parameter of the method is an object of type DynamicPropertyRegistry in which you set the database connection data required by Spring Boot. This is the code:

@DynamicPropertySource
private static void setupProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", mySQLContainer::getJdbcUrl);
    registry.add("spring.datasource.username", mySQLContainer::getUsername);
    registry.add("spring.datasource.password", mySQLContainer::getPassword);
}

As you can see, you only need a little plumbing work.

Running the test

We’re all set, so let’s use the container in a test. The following one checks if the container is running:

@Test
void testMySQLContainerIsRunning() {
    assertThat(mySQLContainer.isRunning()).isTrue();
}

Although the test doesn’t seem like a big deal, it is enough to ensure everything is in place.

If you examine the test’s output (the above screenshot belongs to IntelliJ), you will discover what’s happening behind the scenes. Testcontainers creates and runs the container, and then waits for the service it contains to be fully operational. This process takes a few seconds because MySQL has to start and configure itself. Only then will Testcontainers let JUnit to continue with the test execution cycle, which involves creating the Spring context.

This message is very interesting:

Waiting for database connection to become available at jdbc:mysql://localhost:49164/testcontainer using query 'SELECT 1'

Besides revealing the connection URL, it indicates the technique used to check MySQL availability. It consists of repeatedly executing a test query (SELECT 1) until the response is returned or the maximum startup timeout, set by default at 120 seconds, is exceeded. You can change this generous value with the MySQLContainer#withStartupTimeoutSeconds method.

With the tool DockStation you will see the following during execution.

The most prominent element in the screenshot is the log of the MySQL container, but if you look at the menu on the left, you will see two containers. Ryuk is the sinister name of the image Testcontainers uses to manage the containers. Its primary mission is to destroy them once the test execution ends.

Initial script for MySQL

The official MySQL image can execute SQL scripts with the initial configuration if you place them in the /docker-entrypoint-initdb.d folder. These scripts let you to create users, databases, records, stored procedures, etc. They are only executed the first time a new container is started.

In a Docker File, you can copy a script in the folder mentioned earlier with the following command:

COPY init.sql /docker-entrypoint-initdb.d/

With MySQLContainer you can achieve the same by calling the withInitScript method. Just provide the relative path to the file, considering that the root is the /src/test/resources/ folder of the project:

    @Container
    private static final MySQLContainer mySQLContainer = new MySQLContainer<>("mysql:8.0.30")
            .withDatabaseName("testcontainer")
            .withUsername("user")
            .withPassword("pass")
            .withInitScript("init.sql");

This is the content of init.sql, it creates a table with a primary key:

USE testcontainer;

CREATE TABLE tests (
    id BIGINT AUTO_INCREMENT PRIMARY KEY
)

Let’s improve MysqlContainerTest to verify that init.sql was executed:

@Autowired
private DataSource dataSource;    

@Test
void testTableExists() throws SQLException {
    try (Connection conn = dataSource.getConnection();
         ResultSet resultSet = conn.prepareStatement("SHOW TABLES").executeQuery();) {
         resultSet.next();

        String table = resultSet.getString(1);
        assertThat(table).isEqualTo("tests");
   }
}

testTableExists checks the existence of the tests table by using the standard JDBC API. It obtains a connection from the DataSource bean, executes a query, and collects the result in a ResultSet. All of this is done within a try-with-resources block that guarantees the closure of the opened resources.

Test datasets with @Sql

Few tests you can write if you don’t have a test dataset to rely on. Of course, you can include it in MySQL configuration scripts, but different tests will likely to require different datasets. No problemo: Spring satisfies this need with the powerful @Sql annotation.

Although the sample project doesn’t use @Sql, this annotation deserves a short explanation. In a nutshell, @Sql executes SQL scripts and statements during integration testing. By default, the class-level @SQL executes its SQL before each test (you can change this behavior by setting the executionPhase property):

@SpringBootTest
@Sql(scripts = {"/scripts/dataset1.sql", "/scripts/dataset2.sql"})
class SomeRandomServiceTest {

The preceding snippet executes the scripts /src/test/resources/scripts/dataset1.sql and /src/test/resources/scripts/dataset2.sql in that order.

@Sql method-level declarations override class-level declarations unless you change the behavior with @SqlMergeMode. In this example, Spring will first execute the SQL of the @Sql annotation of the class, and then the SQL of the method annotation:

@SpringBootTest
@Sql(scripts = {"/scripts/dataset1.sql", "/scripts/dataset2.sql"})
class SomeRandomServiceTest {
    
    @Test
    @Sql("/scripts/dataset3.sql")
    @SqlMergeMode(SqlMergeMode.MergeMode.MERGE)
    void testFindAll() {

Sharing a container between tests classes

Creating and starting a container takes a short time, but the initial MySQL configuration doesn’t, as you may have noticed if you ran the examples. It takes about ten seconds on the computer where I wrote this post—an eternity if you launch the tests frequently while coding. To top it off, if you have several test classes that need the same container and you define it in each test class, a new container is created and executed for each.

You can speed things up if you build an image containing the tables and the basic data—but it’s not enough.

How to reduce this waste of time? Do we write all the tests in a huge class? 🤔

The answer is to take the container configuration to a superclass. This way, you centralize the configuration and only one container will be started for all the tests written in the child classes.

This superclass declares and setup the field mySQLContainer:

@SpringBootTest
public class MySQLContainerBaseTest {

    @Autowired
    protected DataSource dataSource;
    
    protected static final MySQLContainer mySQLContainer = new MySQLContainer<>("mysql:8.0.30")
            .withDatabaseName("testcontainer")
            .withUsername("user")
            .withPassword("pass");
    static {
        mySQLContainer.start();
    }

    @DynamicPropertySource
    private static void setupProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", mySQLContainer::getJdbcUrl);
        registry.add("spring.datasource.username", mySQLContainer::getUsername);
        registry.add("spring.datasource.password", mySQLContainer::getPassword);
    }

}

If you compare this class with MySQLContainerTest, you will notice that the @Testcontainers and @Container annotations are gone. If you leave them, Testcontainers will still create a container for each child class, which is what you want to avoid. Now the container creation will be explicitly done only once in a static block (line 12). Our pal Ryuk will take care of the destruction of the container.

Let’s check that the above technique works by creating two new classes that extend MySQLContainerBaseTest:

class MySQLContainerClass1Test extends MySQLContainerBaseTest {

    @Test
    void testMySQLContainerIsRunning() {
        assertThat(mySQLContainer.isRunning()).isTrue();
    }

}
class MySQLContainerClass2Test extends MySQLContainerBaseTest {

    @Test
    void testMySQLContainerIsRunning() {
        assertThat(mySQLContainer.isRunning()).isTrue();
    }

}

If you run MySQLContainerClass1Test and MySQLContainerClass2Test together, you will see in the output that only one container is started. But if you run them together with MySQLContainerTest, you will have two containers: the one declared in that class and the one declared in MySQLContainerBaseTest. Both will coexist until all the tests finish because, remember, Testcontainers exposes each container on a different random port.

Reusing containers

You have learned how to write a superclass to share a container between several test classes. However, a new container is created and started each time you jointly execute the tests of one or more of these classes. For this reason, another point of improvement is to reuse the container between different batches of test execution. In other words, instead of destroying the container after all the tests are finished, keep it running to reuse it for future tests.

Looking at the GenericContainer API it seems that calling withReuse(true) enforces the desired behavior:

@SpringBootTest
@Testcontainers
class MySQLContainerTest {

    @Container
    private static final MySQLContainer mySQLContainer = new MySQLContainer<>("mysql:8.0.30")
       .withDatabaseName("testcontainer")
       .withUsername("user")
       .withPassword("pass")
       .withInitScript("mysql/schema.sql")
       .withReuse(true);

Unfortunately, it is more complicated; calling withReuse(true) doesn’t change anything. You must also remove the @Container and @Testcontainers annotations and switch to manually managing the container. It is the same thing we did in the MySQLContainerBaseTest class, so let’s call withReuse on the object mySQLContainer:

@SpringBootTest
public class MySQLContainerBaseTest {

    @Autowired
    protected DataSource dataSource;

    protected static final MySQLContainer mySQLContainer = new MySQLContainer<>("mysql:8.0.30")
            .withDatabaseName("testcontainer")
            .withUsername("user")
            .withPassword("pass")
            .withReuse(true);
    static {
        mySQLContainer.start();
    }

It still doesn’t work! One final step: enable the reuse capability in the Testcontainers configuration file. It is located in the user’s folder in the operating system, as explained in the documentation. For instance, the file on my computer (Linux) is /home/dani/.testcontainers.properties:

#Modified by Testcontainers
#Sun Oct 02 14:52:55 CEST 2022
docker.client.strategy=org.testcontainers.dockerclient.UnixSocketClientProviderStrategy

testcontainers.reuse.enable = true

The previous steps are explained here.

A container is created and started the first time you run a class that inherits from MySQLContainerBaseTest. But when the tests finish, the container remains running. Consequently, the next time you run the same tests, they will be faster because the required container is already available—a perfect plan.

Reusability raises two good questions:

  • What if you stop the container? A new one is created instead of starting the existing one. Reuse is only possible if the containers generated by Testcontainers remain up and running.
  • What happens when the configuration changes because, for example, we rename the database? In this case, Testcontainers detects the change and starts a new container.

Finally, I want to highlight a crucial detail: it is our responsibility to stop and remove the containers that remain running after the execution of the tests. For this reason, in integration environments (Jenkins, Bamboo, etc.) it’s not recommended to enable the reuse feature, at least without implementing an automatic mechanism that handles the containers when they are no longer needed. And, of course, in your development machine you should also be careful with those containers—it may become a dump of useless containers.

The quick setup for relational databases

I will show you a shortcut now that you have the basic knowledge. Testcontainers provides a super-simple way to run a container for some relational databases such as MySQL and PostgreSQL.

spring.datasource.url=jdbc:tc:mysql:8.0.30:///testcontainer

The above line is the typical JDBC-accessible database URL configuration you would put in Spring Boot’s application.properties file for testing. It has two peculiarities: the word "tc"after "jdbc" and the MySQL version after "mysql". The string "testcontainer" is the name you want to give to the database.

That’s all. Testcontainers starts a new MySQL 8.0.30 container as soon as Spring Boot starts, and Ryuk will shut it down when it won’t longer necessary. You don’t even have to annotate the test class with @Testcontainer. The trick? The JDBC driver that handles URLs of type "jdbc:tc" is the ContainerDatabaseDriver class. This class acts as a proxy for the real JDBC driver and spells the magic of Testcontainers when Spring —or whoever— creates the DataSource.

You keep the ability to run a startup script. Just specify its relative path inside the classpath with the TC_INITSCRIPT property:

spring.datasource.url=jdbc:tc:mysql:8.0.30:///testcontainer?TC_INITSCRIPT=init.sql

Regarding performance, if you run several test classes and different Spring contexts are required, Testcontainers creates a container for each. Fortunally, you can share the container between all tests by enabling the TC_DAEMON flag:

spring.datasource.url=jdbc:tc:mysql:8.0.30:///testcontainer?TC_DAEMON=true

Attention: sharing refers to the tests launched in the same execution batch. When they finish, Ryuk will remove the container. Please don’t get confused with the container reutilization you achieve with the withReuse method—URL-based configuration doesn’t support the reuse feature.

You can find the below example in the project. To avoid interference with other tests, I declared the database URL with @SpringBootTest.

@SpringBootTest(properties = "spring.datasource.url=jdbc:tc:mysql:8.0.30:///testcontainer?TC_INITSCRIPT=init.sql")
class MySQLContainerUrlTest {

    @Test
    void testTableExists(@Autowired DataSource dataSource) throws SQLException {
        TableTestAssertion.assertTableExists(dataSource);
    }

}

Faster databases

¡Full steam ahead! The Docker tmpfs option mounts a container folder in the host RAM, much faster than any SSD drive. The downside is that the contents of that folder will be lost when the container stops.

Knowing the preceding Docker feature, you can save more time. If you redirect to memory the folder where the database stores the information, you save the time spent reading and writing to the hard drive. You can do this without any problem because it doesn’t matter if the data is lost.

The Testcontainers team has caught this optimization and lets you to configure tmpfs folders via the GenericContainer#withTmpFs method:

.withTmpFs(Map.of("/var/lib/mysql", "rw"))

You can also set the tmpfs folder in the URL:

spring.datasource.url=jdbc:tc:mysql:8.0.30:///testcontainer?TC_INITSCRIPT=init.sql&TC_TMPFS=/var/lib/mysql:rw

Don’t expect a noticeable performance improvement unless the tests have large datasets and query the database intensively.

Generic images

MySQL is one of the many modules that Testcontainers has, but you can use any dockerized service, even if it doesn’t have a module. Modules are just a helper, and you can configure any container with the GenericContainer class.

This new test class —it doesn’t use Spring— instantiates a container of the latest Apache web server image, and checks whether the container is running:

@Testcontainers
class ApacheWebContainerTest {

    @Container
    private static final GenericContainer httpdContainer = new GenericContainer<>("httpd:latest");

    @Test
    void testApacheContainerIsRunning() {
        assertThat(httpdContainer.isRunning()).isTrue();
    }

}

GenericContainer provides many public methods. MySQLContainer, as a specialization of GenericContainer, inherits them. I have summarized the most important ones in this table according to my experience:

withExposedPortsThe ports of the container to expose. They are exposed outwards through random ports.
withEnvSet an environment variable.
withCopyFileToContainerCopies a file to the container before it starts. Equivalent to Dockerfile’s COPY command.
withCommandOverwrites the command that the container executes at startup.
waitingForDefine with WaitStrategy how to detect the availability of the service running inside the container.
withFileSystemBindAdds a file system binding (from the host to the container).
withClasspathResourceMappingMap a resource (file or directory) on the classpath to a path inside the container.
withTmpFsYou have already used it: it mounts folders in memory.

Let’s improve the test by exposing port 80 of the web server:

@Container
private static final GenericContainer httpdContainer = new GenericContainer<>("httpd:latest")
                                                                                       .withExposedPorts(80);

@Test
void testGetResponseIsOk() throws Exception {
  String address = "http://" + httpdContainer.getHost() + ":" + httpdContainer.getMappedPort(80);
  HttpRequest localhost = HttpRequest.newBuilder(new URI(address)).build();

  HttpResponse<Void> response = HttpClient.newHttpClient()
           .send(localhost, HttpResponse.BodyHandlers.discarding());

   assertThat(response.statusCode()).isEqualTo(HttpStatus.SC_OK);
}

The URL of the web server, including the random port chosen by Testcontainers, is built by asking httpdContainer.

testGetResponseIsOk validates with the Java 11 networking API that the call to the root URL of the web server returns the status code 200.

The next test defines a MySQL container with the same features as the previous examples:

@SpringBootTest
@Testcontainers
class MySQLCustomContainerTest {

    @Autowired
    private DataSource dataSource;

    @Container
    private static final GenericContainer mySQLContainer = new GenericContainer<>("mysql:8.0.30")
            .withEnv("MYSQL_ROOT_PASSWORD", "pass")
            .withEnv("MYSQL_DATABASE", "testcontainer")
            .withEnv("MYSQL_USER", "user")
            .withEnv("MYSQL_PASSWORD", "pass")
            .withExposedPorts(3306)
            .waitingFor(Wait.forLogMessage(".*mysqld: ready for connections.*", 2))
            .withCopyFileToContainer(MountableFile.forClasspathResource("init.sql"), "/docker-entrypoint-initdb.d/schema.sql");

    @DynamicPropertySource
    private static void setupProperties(DynamicPropertyRegistry registry) {
        String url = "jdbc:mysql://localhost:" + mySQLContainer.getMappedPort(3306) + "/testcontainer";
        registry.add("spring.datasource.url", () -> url);
        registry.add("spring.datasource.username", () -> "user");
        registry.add("spring.datasource.password", () -> "pass");
    }

    @Test
    void testTableExists() throws SQLException {
        try (Connection conn = dataSource.getConnection();
             ResultSet resultSet = conn.prepareStatement("SHOW TABLES").executeQuery();) {
            resultSet.next();

            String table = resultSet.getString(1);
            assertThat(table).isEqualTo("tests");
        }
    }

}

MySQLCustomContainerTest replicates the configuration performed internally by the MySQL module. The credentials and the database name are sent to the container with environment variables (I know this because it is stated in the documentation of the image). The code also copies the init.sql script and sets the criteria that decide whether MySQL is ready to accept connections.

If you skip the latter, as soon as the container is started, the tests will run before the database is operational. The applied strategy isn’t as fancy as the one used by the module I mentioned (running a test query), but it is simple and adaptable to other services. You don’t even have to write code to implement it.

.waitingFor(Wait.forLogMessage(".*mysqld: ready for connections.*", 2))

The previous line decides that the container is ready when a string containing "mysqld: ready for connections." appears twice in the container’s log (after it appears for the first time, MySQL restarts to execute the init script). The drawback of this strategy is its embrittlement, since the message could be different in other versions of the Docker image.

Other features

We have covered the fundamentals of Testcontainers—but the rabbit hole is deeper than you think! This section outlines three cool features.

Dockerfile

You can create images on the fly from a Dockerfile:

@Container
private static final GenericContainer containerFromDockerfile = new GenericContainer(
        new ImageFromDockerfile()
                .withFileFromClasspath("Dockerfile", "docker/Dockerfile"));

In the preceding code, the image is defined in /src/test/resources/docker/Dockerfile.

Docker compose

It is possible to run the services specified in a docker-compose file:

@Container
public static DockerComposeContainer containersFromCompose =
        new DockerComposeContainer(new File("src/test/resources/docker/docker-compose.yml"))
Networking

You can place multiple containers on the same network so they can connect to each other using their service names (a feature of Docker networks). Here is an example from the official documentation:

@Test
public void testNetworkSupport() throws Exception {
  try (
    Network network = Network.newNetwork();
    GenericContainer<?> foo = new GenericContainer<>(TestImages.TINY_IMAGE)
        .withNetwork(network)
        .withNetworkAliases("foo")
        .withCommand(
            "/bin/sh",
            "-c",
            "while true ; do printf 'HTTP/1.1 200 OK\n\nyay' | nc -l -p 8080; done"
        );
    GenericContainer<?> bar = new GenericContainer<>(TestImages.TINY_IMAGE)
        .withNetwork(network)
        .withCommand("top")
  ) {
    foo.start();
    bar.start();

    String response = bar.execInContainer("wget", "-O", "-", "http://foo:8080").getStdout();
    assertThat(response).as("received response").isEqualTo("yay");
  }
}

The test executes a command inside the bar container that calls the foo web server via HTTP. The call reaches the suitable container because it is on the same network as bar and aliased as foo.

Source code

The project is available on GitHub.

Other posts in English

JSP and Spring Boot

Spring Framework: asynchronous methods with @Async, Future and TaskExecutor

Spring Framework: event handling

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.