Testcontainers is a library that helps us write unit tests for applications and databases running on a Docker container. Testcontainers will help us run applications or databases using Docker Images, then we will use the plugins it supports to implement the unit test code we want. Testcontainers supports many different languages, in addition to Java, it also has versions for Go, .Net, Node.js, Python, Rust, Haskell. In this tutorial, I will show you how to use Testcontainers for Java to write unit tests for the MySQL database manipulation part of a simple Java application.
First, I will create a new Maven project as an example:
I will declare Testcontainers and MySQL JDBC driver dependency for this project as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.32</version> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <version>1.17.6</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>mysql</artifactId> <version>1.17.6</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <version>1.17.6</version> <scope>test</scope> </dependency> |
As an example, I will define a student table in MySQL database with the following simple structure:
1 2 3 4 5 |
CREATE TABLE `students` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(45) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
The data in the student table is as follows:
To get information in this student table, I will write JDBC code as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package com.huongdanjava.testcontainers; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; public class StudentService { public List<Student> getAllStudents(Connection connection) throws SQLException { List<Student> students = new ArrayList<>(); Statement statement = connection.createStatement(); ResultSet rs = statement.executeQuery("SELECT * FROM students"); while (rs.next()) { Student student = new Student(rs.getInt("id"), rs.getString("name")); students.add(student); } return students; } } |
with the Student class with the following content:
1 2 3 4 5 |
package com.huongdanjava.testcontainers; public record Student(int id, String name) { } |
The main class to run this application is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package com.huongdanjava.testcontainers; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.List; public class Application { public static void main(String[] args) throws SQLException { Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/example", "root", "123456"); StudentService studentService = new StudentService(); List<Student> students = studentService.getAllStudents(connection); students.forEach(s -> { System.out.println("ID: " + s.id() + ", name: " + s.name()); }); } } |
Result:
To write unit tests for the getAllStudents() method to get student information from the database, in the past, you could use the H2 in-memory database. With this approach, our unit test code doesn’t actually run against the database we are using for the application. And so we can run into issues when running the application that the unit test code fails to detect.
Now you can use Testcontainers to solve this shortcoming. Testcontainer will help us to run unit test code against the real database that our application is using.
The disadvantage of this solution is that you must have Docker installed on the machine running the unit test, but this inconvenience is not a big problem, right?
To write unit tests for the StudentService class using Testcontainers, I will create a new class and annotate this class with the @Testcontainers annotation of the Testcontainer as follows:
1 2 3 4 5 6 7 8 |
package com.huongdanjava.testcontainers; import org.testcontainers.junit.jupiter.Testcontainers; @Testcontainers public class StudentServiceTest { } |
This @Testcontainers annotation has a disabledWithoutDocker attribute that helps our unit test code with Testcontainers will automatically disable if the machine running the unit test does not have that Docker installed! The default value of this property is false which means that our unit test code will not be disabled if the machine running the unit test does not have Docker installed. Depending on the situation, you can add this attribute if you want. My machine is installing Docker, so I don’t need to declare this attribute.
Now we will declare the container that we will need to run before the unit test code is executed with the @Container annotation. My example will be like this:
1 2 3 4 5 6 |
@Container public static MySQLContainer<?> container = new MySQLContainer<>("mysql:latest") .withDatabaseName("example") .withPassword("123456") .withInitScript("db.sql") .withReuse(true); |
By default, the MySQL container with Testcontainer will run with the user “root” and the password “test”. For my example, I changed the password to “123456” as you can see. My database name is “example”.
I also declare an initial SQL script so that when running the container, Testcontainers will automatically create a new students table and insert data into this table. The contents of the db.sql file located in the src/test/resources directory are as follows:
1 2 3 4 5 6 7 8 |
CREATE TABLE `students` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(45) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO students SET name='Khanh'; INSERT INTO students SET name='Thanh' |
The withReuse() method makes it possible to reuse the container we created earlier.
To run the container that we have declared above, you can call the start() method in the annotated method with the @BeforeAll annotation of JUnit 5 as follows:
1 2 3 4 5 6 7 |
@BeforeAll public static void init() throws SQLException { container.start(); connection = DriverManager.getConnection(container.getJdbcUrl(), "root", "123456"); } |
As you can see, I also initialized the Connection object from the information of the MySQL container that I ran.
Now, you can write unit tests for the getAllStudents() method as follows:
1 2 3 4 5 |
@Test public void testGetAllStudents() throws SQLException { List<Student> allStudents = studentService.getAllStudents(connection); Assertions.assertTrue(allStudents.size() == 2); } |
My entire unit test code for the StudentService class is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
package com.huongdanjava.testcontainers; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.containers.MySQLContainer; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @Testcontainers public class StudentServiceTest { private static Logger log = LoggerFactory.getLogger(StudentServiceTest.class); private StudentService studentService = new StudentService(); private static Connection connection; @Container public static MySQLContainer<?> container = new MySQLContainer<>("mysql:latest") .withDatabaseName("example") .withPassword("123456") .withInitScript("db.sql") .withReuse(true); @BeforeAll public static void init() throws SQLException { container.start(); connection = DriverManager.getConnection(container.getJdbcUrl(), "root", "123456"); } @Test public void testGetAllStudents() throws SQLException { List<Student> allStudents = studentService.getAllStudents(connection); Assertions.assertTrue(allStudents.size() == 2); } } |
Result: