Integration Testing with Spring Boot, MySQL and Testcontainers

Welcome, and thank you for choosing this tutorial! If you’re an absolute beginner looking to learn about integration testing for Spring Boot applications with a MySQL database server, then you’re in the right place. As you proceed through this tutorial, remember that everyone was once a beginner, and the only silly question is the one you don’t ask. So feel free to pause, ponder, and ask as we progress together.

This tutorial aims to introduce you to Testcontainers – a fantastic Java library that simplifies integration tests by providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.

We will be working with a simple Spring Boot application that communicates with a MySQL database server, all running locally on your machine. Don’t worry if that sounds complicated – we’ll walk through each step together, explaining what everything does and how it all fits together.

By the end of this tutorial, you will have gained a practical understanding of how to use Testcontainers for integration testing of a Spring Boot application. You will see how easy and powerful it is to simulate a MySQL database server in your tests, making them more reliable and maintainable.

We’ve got a lot to cover, so let’s dive right in!

What are Testcontainers?

Let’s start with understanding what Testcontainers are. Testcontainers is a Java library designed to simplify your testing experience. This library helps you set up and tear down disposable instances of specific software – most commonly, databases like MySQL, or web browsers like Selenium – in Docker containers. This allows us to write tests in Java which interact with these resources, much as we would in a live production environment. With Testcontainers, we don’t have to worry about setting up and managing these pieces of software ourselves, making our lives a lot easier!

Why Use Testcontainers for Integration Testing?

Now, why would we want to use Testcontainers for integration testing? When we’re testing an application, it’s incredibly important that our tests run in an environment that closely resembles our production environment. This becomes even more vital for integration tests. In these tests, we’re checking how different parts of our application work together, often interacting with external systems like databases.

In the real world, our application will be talking to a real database – it won’t know or care whether that database is on the same machine or halfway around the world. So, to make our integration tests truly valuable, we should strive to mimic this situation as closely as possible. Testcontainers helps us do exactly that by providing a real database (or another service) running inside a Docker container. This helps us catch and fix issues that we might miss with a simpler, less realistic setup.

How Do Testcontainers Work?

So, how do Testcontainers actually work? The answer is Docker! Docker is a platform that allows us to “containerize” our applications along with their dependencies, running them in isolated environments called containers.

When you run your tests, Testcontainers will use Docker to pull the necessary images (like MySQL, for example), create new containers from these images, and run these containers. Your test code can then interact with these containers as if they were the real services, connecting to a database or making web requests.

Once the tests are complete, Testcontainers will automatically stop and remove the containers, ensuring that your testing environment is kept clean and ready for the next run.

In the upcoming sections, we will delve into the practical use of Testcontainers, where we will set up a real MySQL database in a container for our Spring Boot application’s integration tests. By the end of this tutorial, you will have a good understanding of how to use Testcontainers in your own projects.

To learn more about Docker, please check out my Docker tutorials for beginners.

About Spring Boot Application

In this tutorial, we will create a very basic Spring Boot application that exposes a REST API endpoint for managing user details. Specifically, it will have a single /users endpoint that accepts a POST request containing User details such as firstName, lastName, email, and password. When a request is made, the application will store these details in a MySQL database using Spring Data JPA, a part of Spring Framework that simplifies database operations. This straightforward application will serve as an ideal platform for demonstrating how to use Testcontainers for integration testing.

Creating a New Spring Boot Application

First, we need to set up the Spring Boot project. You can do this using Spring Initializr https://start.spring.io/ or through your preferred IDE. Select the following dependencies:

  • Spring Web
  • Spring Data JPA
  • MySQL Driver

After generating the project, open it in your IDE.

Setting up the Database

Next, we’ll set up the MySQL database in our application.properties file located in src/main/resources.

spring.datasource.url=jdbc:mysql://localhost:3306/testdb
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.hibernate.ddl-auto=update

Note: Remember to replace the above values with your actual MySQL database details.

Creating the User Entity

Let’s create the User entity class. Navigate to the main package and create a new class named User.

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String firstName;
    private String lastName;
    private String email;
    private String password;

    // Getters and Setters
}

Creating the User Repository

Next, we create a User Repository interface that extends JpaRepository.

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

Creating the REST API

Now, let’s create a simple POST API to save the user details in the database.

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    private final UserRepository userRepository;

    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @PostMapping("/users")
    public User createUser(@RequestBody User user) {
        return userRepository.save(user);
    }
}

This is the basic setup of our application. Now let’s proceed to setup Testcontainers for integration testing.

Setting up Testcontainers

First, add the following Testcontainers and Spring Boot Test dependencies in your build.gradle or pom.xml file.

Gradle:

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.testcontainers:mysql:1.16.0'
testImplementation 'org.testcontainers:junit-jupiter:1.16.0'

Maven:

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

Creating a Test Configuration

Create a Test Configuration class to start a MySQL container before tests and stop it after tests.

This step creates a Test Configuration for our Spring Boot tests. It’s a class that provides additional beans or modifications of existing ones for the application context, specifically during testing.

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;

@TestConfiguration
public class DatabaseTestConfiguration {

    @Bean
    public MySQLContainer<?> mySQLContainer() {
        MySQLContainer<?> container = new MySQLContainer<>(DockerImageName.parse("mysql:8.0.26"))
                .withDatabaseName("testdb")
                .withUsername("test")
                .withPassword("test");
        container.start();
        return container;
    }
}

@TestConfiguration: This annotation indicates that the class should be picked up by Spring Boot’s automatic configuration during testing. It can be thought of as a kind of @Configuration specifically for tests.

@Bean: This annotation is a method-level annotation in Spring Framework. A Java method annotated with the @Bean annotation signifies that this method produces a bean to be managed by the Spring container.

MySQLContainer<?>: Testcontainers provides a MySQLContainer class that represents a Docker container running a MySQL database. The <?> is a generic type that allows for subclasses of the container. It’s often just MySQLContainer.

DockerImageName.parse("mysql:8.0.26"): This specifies the Docker image to be used to create the container. In this case, it’s MySQL version 8.0.26.

withDatabaseName("testdb"), withUsername("test"), withPassword("test"): These methods are configuring the container with the name of the database to be created when the container starts, and the username and password for connecting to that database.

container.start(): This starts the MySQL container. It’s important to call this method to make sure the container is running before the tests start.

The mySQLContainer() method is creating and returning an instance of MySQLContainer. During tests, this instance will be managed by Spring and can be injected into tests where it’s needed.

So, the purpose of this Test Configuration is to start a MySQL container that can be used during testing. This is an integral part of using Testcontainers for testing database interactions, as it provides a real MySQL database that’s automatically started and stopped around each test.

Setting up Test Properties

Next, create an application-test.properties file in src/test/resources.

spring.jpa.hibernate.ddl-auto=create-drop

This property is specifically related to the Hibernate framework, which is the default implementation of Spring Data JPA’s underlying ORM (Object-Relational Mapping) technology.

Let’s break down what this configuration does:

  • spring.jpa.hibernate.ddl-auto: This property is used to automatically create (create), update (update), or remove (drop) the database schema based on the entities defined in your application.
  • create-drop: The create-drop value means that Hibernate will generate the necessary schema (tables, constraints, etc.) when the EntityManagerFactory is created (typically when your application starts), and drop the schema when the EntityManagerFactory is closed (typically when your application ends). This is useful in a test scenario as it ensures that each test runs against a clean database.

So, the purpose of this property is to automatically setup and teardown the database schema for each test. This ensures that each test runs in isolation, preventing interferences from previous tests and leaving no artifacts behind after the tests complete. It’s an important part of using an in-memory or containerized database for testing.

Writing the Test

Finally, let’s write the test for our UserController.

import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.AfterEach;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public class UserControllerTest {

    @Autowired
    private DataSource dataSource;

    @Container
    public static MySQLContainer<?> mySQLContainer = new MySQLContainer<>(DockerImageName.parse("mysql:8.0.26"))
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test")
            .waitingFor(Wait.forListeningPort())
            .withEnv("MYSQL_ROOT_HOST", "%");

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

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

    @AfterEach
    void tearDown() {
        if (dataSource instanceof HikariDataSource) {
            ((HikariDataSource) dataSource).close();
        }
    }

    @Test
    public void testCreateUser() {
        User user = new User();
        user.setFirstName("Test");
        user.setLastName("User");
        user.setEmail("[email protected]");
        user.setPassword("testpass");

        User response = restTemplate.postForObject("http://localhost:" + port + "/users", user, User.class);
        assertThat(response).isNotNull();
        assertThat(response.getId()).isNotNull();
    }
}

Let me explain this code part by part:

  1. Imports: These are the packages and classes you will need for the test.
  2. @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT): This annotation is used to specify that we’re bootstrapping the whole Spring Boot application for the test, but with a random server port.
  3. @Testcontainers: This annotation tells JUnit that this class will use Testcontainers. It will ensure that the containers needed by the test are started and stopped automatically.
  4. @Container: This annotation marks the mySQLContainer field to be managed by Testcontainers. It will start the container before the tests and stop it after.
  5. @LocalServerPort: This annotation allows us to inject the HTTP port that got allocated at runtime, because we’re using SpringBootTest.WebEnvironment.RANDOM_PORT.
  6. @Autowired: This annotation is used to automatically inject beans into the test. Here, it’s used to inject the dataSource and the restTemplate.
  7. @DynamicPropertySource: This annotation is used to dynamically add properties to the Spring Environment’s PropertySources. Here, it’s used to update the datasource properties at runtime to point to our MySQL container.
  8. tearDown(): This method is annotated with @AfterEach to specify that it should run after each test. It closes the HikariCP connection pool, which prevents a warning message about using a closed connection.
  9. testCreateUser(): This is the actual test method. It creates a new User, sends a POST request to the /users endpoint, and checks that the response is not null and that the ID of the user in the response is not null.

In short, this is a test class that starts a MySQL container, starts the Spring Boot application, and runs an end-to-end test that interacts with the database through the HTTP endpoint. After the test is run, it cleans up the connection pool and stops the MySQL container.

Final Words

We’ve covered a lot of ground in this tutorial. As a beginner, you’ve taken some significant steps into the world of integration testing using Testcontainers with a Spring Boot application. By now, you should have a good grasp of what Testcontainers is, why it’s beneficial for integration testing, and how it works with Docker.

Here are a few important takeaways from this tutorial:

  1. Testcontainers is a powerful tool for creating realistic integration tests: By using real instances of software like databases, we can write tests that are as close as possible to the conditions our application will encounter in production.
  2. Integration testing is made simpler with Testcontainers: With Testcontainers, you can write tests that interact with external resources like databases or web servers without having to manually install and manage those resources yourself.
  3. Docker is the driving force behind Testcontainers: The ability of Testcontainers to isolate and manage software in a consistent environment is provided by Docker, a platform that packages and runs applications in isolated containers.
  4. Spring Boot’s testing support complements Testcontainers: Spring Boot provides excellent support for writing and running tests, and as we’ve seen in this tutorial, this works perfectly with Testcontainers to create a seamless testing experience.

In conclusion, the ability to use Testcontainers for integration testing is a great skill to have as a developer. It can help to catch issues early, reduce the complexity of your testing setup, and ensure that your tests run in an environment that closely mirrors production. As you continue your learning journey, I encourage you to explore further, applying what you’ve learned in this tutorial to other types of software and testing scenarios.

Take your Spring Boot testing skills to new heights by exploring our comprehensive tutorials on the Testing Java Code page. Learn proven strategies and techniques for effectively testing Spring Boot applications, ensuring their stability and robustness.