[JAVA] Start MySQL container only during Spring Boot + MyBatis test execution with Testcontainers

Introduction

You can use Testcontainers (https://www.testcontainers.org/) to start a MySQL container only while testing JUnit.

Sample code SimpleMySQLTest.java It seems to be quite easy to use, but I had a hard time trying it with Spring Boot + MyBatis, so I will summarize how it works.

First, run Spring Boot + MyBatis normally

Before trying the test with Testcontainers, write some working code.

Create code for MyBatis

Create three files, UserRepositoryImpl.java, UserMapper.java, and UserMapper.xml.

UserRepositoryImpl.java


package springdockerexample.infrastructure.user;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import springdockerexample.domain.user.Name;
import springdockerexample.domain.user.User;
import springdockerexample.domain.user.UserRepository;
import springdockerexample.domain.user.Users;

import java.util.List;

@Repository
public class UserRepositoryImpl implements UserRepository {
  @Autowired
  private UserMapper mapper;

  @Override
  public Users findAll() {
    List<User> users = mapper.selectAll();
    return new Users(users);
  }
}

UserMapper.java


package springdockerexample.infrastructure.user;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import springdockerexample.domain.user.Name;
import springdockerexample.domain.user.User;

import java.util.List;

@Mapper
public interface UserMapper {
  List<User> selectAll();
}

UserMapper.xml


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="springdockerexample.infrastructure.user.UserMapper">

    <resultMap id="user" type="springdockerexample.domain.user.User">
        <result property="name.value" column="name"/>
        <result property="age.value" column="age"/>
    </resultMap>

    <select id="selectAll" resultMap="user">
        SELECT name, age FROM users
    </select>
</mapper>

Prepare property file

Write the DB connection information in application.yaml.

application.yaml


spring:
  datasource:
    url: jdbc:mysql://localhost/mydb
    username: user
    password: password
    driverClassName: com.mysql.cj.jdbc.Driver

Prepare test data

Prepare the data for development and testing in a file.

sql:src/test/resources/docker-entrypoint-initdb.d/init.sql


USE `mydb`;

CREATE TABLE `users` (
  `name` VARCHAR(255) NOT NULL,
  `age` int NOT NULL
);

INSERT INTO `users` (`name`, `age`) VALUES
('Alice', 20),
('Bob', 30);

Launch container locally

To check the operation of Spring Boot + MyBatis, start MySQL with Docker Compose.

At this time, by mounting the SQL file prepared earlier on docker-entrypoint-initdb.d, the data will be inserted automatically.

docker-compose.yaml


version: '3'
services:

  my-db:
    image: mysql:5.7.25
    ports:
      - 3306:3306
    volumes:
      - ./src/test/resources/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: mydb
      MYSQL_USER: user
      MYSQL_PASSWORD: password

Operation check

If you include spring-dev-tools, it will boot with ./mvnw spring-boot: run.

$ ./mvnw spring-boot:run
$ curl localhost:8080/users
{"users":[{"name":"Alice","age":20},{"name":"Bob","age":30}]}

It is working safely.

As you can see, it is possible to test with a container started with Docker Compose, but it is not possible to restart the container for each test case.

We use Testcontainers to launch containers at will during JUnit testing.

Start container only while JUnit is running in Testcontainers

MySQL startup for testing + connection information settings

For testing, refer to "Prepare a disposable database container with test containers to test the Spring Boot application" Create the following files to start the MySQL container and set the connection information to the Context.

MySQLContainerContextInitializer.java


package springdockerexample.testhelper;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;

public class MySQLContainerContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
  private static final String MYSQL_IMAGE = "mysql:5.7.25";

  private static final String DATABASE_NAME = "mydb";
  private static final String USERNAME = "user";
  private static final String PASSWORD = "password";
  private static final int PORT = 3306;

  private static final String INIT_SQL = "docker-entrypoint-initdb.d/init.sql";
  private static final String INIT_SQL_IN_CONTAINER = "/docker-entrypoint-initdb.d/init.sql";

  private static final Logger LOGGER = LoggerFactory.getLogger(MySQLContainerContextInitializer.class);

  private static final MySQLContainer MYSQL = (MySQLContainer) new MySQLContainer(MYSQL_IMAGE)
          .withDatabaseName(DATABASE_NAME)
          .withUsername(USERNAME)
          .withPassword(PASSWORD)
          .withExposedPorts(PORT)
          .withLogConsumer(new Slf4jLogConsumer(LOGGER))
          .withClasspathResourceMapping(INIT_SQL, INIT_SQL_IN_CONTAINER, BindMode.READ_ONLY);

  static  {
    MYSQL.start();
  }

  @Override
  public void initialize(ConfigurableApplicationContext context) {
    String mysqlJdbcUrl = MYSQL.getJdbcUrl();
    TestPropertyValues.of("spring.datasource.url=" + mysqlJdbcUrl)
            .applyTo(context.getEnvironment());
  }
}

I will explain this content in order.

MySQL container settings

  private static final MySQLContainer MYSQL = (MySQLContainer) new MySQLContainer(MYSQL_IMAGE)
          .withDatabaseName(DATABASE_NAME)
          .withUsername(USERNAME)
          .withPassword(PASSWORD)
          .withExposedPorts(PORT)
          .withLogConsumer(new Slf4jLogConsumer(LOGGER))
          .withClasspathResourceMapping(INIT_SQL, INIT_SQL_IN_CONTAINER, BindMode.READ_ONLY);

Here, the settings that are almost the same as those written in docker-compose.yaml earlier are written in Java.

The image name is specified in new MySQLContainer (MYSQL_IMAGE), and the database name, user name, password, etc. are also set here.

Also, withClasspathResourceMapping mounts the SQL for DB initialization. It seems that you can mount the MySQL config file as well.

MySQL container launch

  static  {
    MYSQL.start();
  }

I'm starting a MySQL container. MySQL.start () must be executed before MySQL.getJdbUrl () in initialize, which will be described later.

I'm skipping this example, but I think it's better to call MySQL.stop () at the end.

Connection information settings

Since the ** port for connecting to MySQL started by Testcontainers is dynamically assigned **, it is necessary to dynamically set the connection information based on that.

I created a class called MySQLContainerContextInitializer to dynamically change the Spring Boot settings.

The specific setting method is as follows.

  @Override
  public void initialize(ConfigurableApplicationContext context) {
    String mysqlJdbcUrl = MYSQL.getJdbcUrl();
    TestPropertyValues.of("spring.datasource.url=" + mysqlJdbcUrl)
            .applyTo(context.getEnvironment());
  }

By the way, the need to get the connection information to MySQL from the MySQLContainer instance is SimpleMySQLTest.java You can also see from the following part of /test/java/org/testcontainers/junit/SimpleMySQLTest.java).

SimpleMySQLTest.java


    @NonNull
    protected ResultSet performQuery(MySQLContainer containerRule, String sql) throws SQLException {
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setDriverClassName(containerRule.getDriverClassName());
        hikariConfig.setJdbcUrl(containerRule.getJdbcUrl());
        hikariConfig.setUsername(containerRule.getUsername());
        hikariConfig.setPassword(containerRule.getPassword());

        HikariDataSource ds = new HikariDataSource(hikariConfig);
        Statement statement = ds.getConnection().createStatement();
        statement.execute(sql);
        ResultSet resultSet = statement.getResultSet();

        resultSet.next();
        return resultSet;
    }

Test description

Here's the Spring Boot test as usual.

UserRepositoryImplTest.java


package springdockerexample.infrastructure.user;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import springdockerexample.domain.user.UserRepository;
import springdockerexample.domain.user.Users;
import springdockerexample.testhelper.MySQLContainerContextInitializer;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(initializers = { MySQLContainerContextInitializer.class })
public class UserRepositoryImplTest {

  @Autowired
  UserRepository userRepository;

  @Test
  public void test() {
    Users users = userRepository.findAll();
    assertThat(users.count(), is(2));
  }
}

The point is that the class created earlier is specified by @ContextConfiguration (initializers = {MySQLContainerContextInitializer.class}), and MySQL is started and connection information is set.

All you have to do is write a normal JUnit test and you'll have no problems accessing the DB.

Summary

In summary, it's not difficult at all, but in reality it was quite difficult. It may be useful once you have set it up.

Since Testcontainers supports any container other than DB, you can freely perform automatic testing.

The final file structure of the contents of this article is as follows.

$ tree
.
├── pom.xml
├── ...
└── src
    ├── main
    │   ├── java
    │   │   └── springdockerexample
    │   │       ├── SpringDockerExampleApplication.java
    │   │       ├── ...
    │   │       └── infrastructure
    │   │           └── user
    │   │               ├── UserMapper.java
    │   │               └── UserRepositoryImpl.java
    │   └── resources
    │       ├── application.yaml
    │       └── springdockerexample
    │           └── infrastructure
    │               └── user
    │                   └── UserMapper.xml
    └── test
        ├── java
        │   └── springdockerexample
        │       ├── SpringDockerExampleApplicationTests.java
        │       ├── infrastructure
        │       │   └── user
        │       │       └── UserRepositoryImplTest.java
        │       └── testhelper
        │           └── MySQLContainerContextInitializer.java
        └── resources
            └── docker-entrypoint-initdb.d
                └── init.sql

The source code is here.

Recommended Posts

Start MySQL container only during Spring Boot + MyBatis test execution with Testcontainers
Perform transaction confirmation test with Spring Boot
Start web application development with Spring Boot
Implement CRUD with Spring Boot + Thymeleaf + MySQL
Form class validation test with Spring Boot
Test controller with Mock MVC in Spring Boot
Asynchronous processing with regular execution in Spring Boot
Until data acquisition with Spring Boot + MyBatis + PostgreSQL
How to use MyBatis2 (iBatis) with Spring Boot 1.4 (Spring 4)
Create CRUD apps with Spring Boot 2 + Thymeleaf + MyBatis
Settings for connecting to MySQL with Spring Boot + Spring JDBC
Try using DI container with Laravel and Spring Boot
[JUnit 5 compatible] Write a test using JUnit 5 with Spring boot 2.2, 2.3
[JUnit 5] Write a validation test with Spring Boot! [Parameterization test]
Test field-injected class in Spring boot test without using Spring container
Until you start development with Spring Boot in eclipse 1
Until you start development with Spring Boot in eclipse 2
I wrote a test with Spring Boot + JUnit 5 now
Download with Spring Boot
Generate barcode with Spring Boot
Implement GraphQL with Spring Boot
Get started with Spring boot
Hello World with Spring Boot!
Run LIFF with Spring Boot
SNS login with Spring Boot
File upload with Spring Boot
Spring Boot starting with copy
Spring Boot starting with Docker
Set cookies with Spring Boot
Use Spring JDBC with Spring Boot
Add module with Spring Boot
Getting Started with Spring Boot
Send email with spring boot
Implement a simple Web REST API server with Spring Boot + MySQL
02. I made an API to connect to MySQL (MyBatis) from Spring Boot
Sample code to unit test a Spring Boot controller with MockMvc
Image Spring Boot app using jib-maven-plugin and start it with Docker