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.
Before trying the test with Testcontainers, write some working code.
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>
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 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);
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
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.
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.
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.
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.
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;
}
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.
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