Testcontainers: How to use it

Daniel S. Blanco
4 min readMay 12, 2023

--

In multiple of the posts that I have done, I have always used containers to be able to perform the tests. Normally creating a docker-compose.yml that allowed to raise and configure the external systems.

Now we are going to do something similar but to carry it out we will use Testcontainers. It is a Java library that allows us to create Docker instances and manage them based on our interest. In the example we will use the following libraries:

  • org.testcontainers:testcontainers:1.16.0
  • org.testcontainers:junit-jupiter:1.16.0
  • org.testcontainers:mysql:1.16.0
  • org.apache.camel:camel-test-spring-junit5:3.16.0
  • org.springframework.boot:spring-boot-starter-test:2.5.1

To start using it, we will mainly use two annotations:

  • @Testcontainers: that allows us to use Testcontainers with JUnit 5.
  • @Containers: It allows us to indicate the instances of containers that we want to be managed by Testcontainers. Although as we will see later we can even use Testcontainers without it.

Before we start, we must clarify a little how it works. Testcontainers allows us to manage containers of any image we want. Being able to add through its methods a brief configuration. But it also has specific modules to make use of common containers, such as databases, Hashicorp, RabitMQ or Kafka.

Another important issue to take into account is that although we indicate the ports we want to expose, we will not be able to create bindings as it is done in a docker-compose. Instead, the exposed ports will be binded to random ports on our machine. Because of this, we will have to modify the default configuration before testing. So that we can indicate the ports that the systems will use, once they have been started with Testcontainers.

To see how it works, we will use examples made with Apache Camel and Spring Boot. In the first example we will send and receive messages from an ActiveMQ queue. Below I detail the most important components of the example:

  • The GenericContainer instance will allow us to indicate the ActiveMQ image to create and the ports to expose.
  • Create a BeforeAll method that allows to start the container and the external bound port.
  • Modify the configuration of the components that communicate with the external system. In Spring Boot we can do it easily with a method annotated with @DynamicPropertySource.
@Testcontainers
@CamelSpringBootTest
@SpringBootTest(classes = ApacheCamelTestApplication.class)
@Log4j2
public class ApacheActiveMqRouterTest {
@Autowired
ProducerTemplate producer;
@Container
private static GenericContainer container = new GenericContainer("rmohr/activemq").withExposedPorts(61616, 8161);
private static Integer tcpPort;
@BeforeAll
public static void beforeAll() {
container.start();
tcpPort = container.getMappedPort(61616);
}
@DynamicPropertySource
static void replaceProperties(DynamicPropertyRegistry registry) {
registry.add("activemq.broker-url", () -> "tcp://localhost:" + tcpPort);
}
@Test
public void amqTo01() throws InterruptedException {
producer.sendBody("direct:SendToPublic", "mensaje ");
}
}

In the second example, it will be similar in operation to the previous one. With the exception that we will use a specific Testcontainers module. With it, through the MySQLContainer class we will be able to make specific configurations to create the container. How to indicate a script to initialize the database.

@Testcontainers
@CamelSpringBootTest
@SpringBootTest(classes = {ApacheCamelTestApplication.class})
@Slf4j
public class ApacheMySQLRouterTest{
static final DockerImageName MYSQL_57_IMAGE = DockerImageName.parse("mysql:5.7.34");
static MySQLContainer<?> database = new MySQLContainer<>(MYSQL_57_IMAGE)
.withInitScript("scripts/init_mysql.sql")
.withDatabaseName("library").withLogConsumer(new Slf4jLogConsumer(log));
@Autowired
ProducerTemplate producer;
@BeforeAll
public static void beforeAll() throws IOException {
database.start();
}
@DynamicPropertySource
static void databaseProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", database::getJdbcUrl);
registry.add("spring.datasource.username", database::getUsername);
registry.add("spring.datasource.password", database::getPassword);
}
@Test
public void getBookById() throws InterruptedException {
producer.sendBodyAndHeader("direct:getBookById", null, "id", 1);
}
}

Before I finish two things. The first, is that during the realization of the examples I have had problems with different versions of Testcontainers:

  • 1.18: I generate NoSuchMethodError exception associated to the method optionallyMapResourceParameterAsVolume.
  • 1.17: Generate ClassNotFoundException: associated to the class org.testcontainers.shaded.org.apache.commons.lang.StringUtils

And the second thing is that if we must use several containers as it is in this case we can have errors because our application tries to connect to an external system and we do not have its container up. Therefore as a recommendation, we can create a class that contains all the configuration of the containers and from which we will extend.

@SpringBootTest
@Testcontainers
@CamelSpringBootTest
@Slf4j
public class TestcontainersConf {
public static final DockerImageName MYSQL_57_IMAGE = DockerImageName.parse("mysql:5.7.34");
static GenericContainer<?> container = new GenericContainer<>("rmohr/activemq").withExposedPorts(61616, 8161);
static MySQLContainer<?> database = new MySQLContainer<>(MYSQL_57_IMAGE)
.withInitScript("scripts/init_mysql.sql")
.withDatabaseName("library").withLogConsumer(new Slf4jLogConsumer(log));
@BeforeAll
public static void beforeAll() {
container.start();
database.start();
}
@DynamicPropertySource
static void replaceProperties(DynamicPropertyRegistry registry) {
registry.add("activemq.broker-url", () -> "tcp://localhost:" + container.getMappedPort(61616));
registry.add("spring.datasource.url", database::getJdbcUrl);
registry.add("spring.datasource.username", database::getUsername);
registry.add("spring.datasource.password", database::getPassword);
}
}
// ............................
public class ApacheMySQLRouterTest extends TestcontainersConf{
@Autowired
ProducerTemplate producer;
@Test
public void getBookById() throws InterruptedException {
producer.sendBodyAndHeader("direct:getBookById", null, "id", 1);
}
}

I hope it has helped you, and if you are interested, here you have all the source code.

--

--

No responses yet