Testcontainers là một thư viện giúp chúng ta có thể viết unit test cho các ứng dụng, database chạy trên một Docker container. Testcontainers sẽ giúp chúng ta chạy các ứng dụng hoặc database lên sử dụng các Docker Images, sau đó thì chúng ta sẽ sử dụng các plugin mà nó hỗ trợ để implement code unit test theo ý mình muốn. Testcontrainers hỗ trợ cho nhiều ngôn ngữ khác nhau, ngoài Java, nó còn có phiên bản dành cho Go, .Net, Node.js, Python, Rust, Haskell. Trong bài viết này, mình sẽ hướng dẫn các bạn làm thế nào để sử dụng Testcontainers cho Java để viết unit test cho phần thao tác với MySQL database của một ứng dụng Java đơn giản các bạn nhé!
Đầu tiên, mình sẽ tạo mới một Maven project để làm ví dụ:
Mình sẽ khai báo Testcontainers và MySQL JDBC driver dependency cho project này như sau:
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> |
Để làm ví dụ, mình sẽ định nghĩa một table student trong MySQL database với cấu trúc đơn giản như sau:
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; |
Data trong table student như sau:
Để lấy thông tin trong table student này, mình sẽ viết code JDBC như sau:
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; } } |
với class Student có nội dung như sau:
1 2 3 4 5 |
package com.huongdanjava.testcontainers; public record Student(int id, String name) { } |
Class main để chạy ứng dụng này như sau:
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()); }); } } |
Kết quả:
Để viết unit test cho phương thức getAllStudents() lấy thông tin sinh viên từ database, ngày xưa, các bạn có thể sử dụng H2 in-memory database. Với approach này, code unit test của chúng ta không thực sự chạy với database mà chúng ta đang sử dụng cho ứng dụng. Và do đó, chúng ta có thể gặp các issue khi chạy ứng dụng mà code unit test không detect được.
Giờ các bạn có thể sử dụng Testcontainers để giải quyết bất cập này. Testcontainer sẽ giúp chúng ta chạy code unit test với database thực sự mà ứng dụng chúng ta đang sử dụng luôn.
Điểm bất tiện của giải pháp này là các bạn phải có Docker được cài đặt trên máy chạy unit test, thế nhưng điểm bất tiện này không phải là vấn đề lớn phải không các bạn?
Để viết unit test cho class StudentService sử dụng Testcontainers, mình sẽ tạo mới một class và annotate class này với annotation @Testcontainers của Testcontainer như sau:
1 2 3 4 5 6 7 8 |
package com.huongdanjava.testcontainers; import org.testcontainers.junit.jupiter.Testcontainers; @Testcontainers public class StudentServiceTest { } |
Annotation @Testcontainers này có một thuộc tính là disabledWithoutDocker giúp cho code unit test với Testcontainers của chúng ta sẽ tự động disable nếu máy chạy unit test không được cài đặt Docker đó các bạn! Mặc định giá trị của thuộc tính này là false nghĩa là code unit test của chúng ta sẽ không bị disable nếu máy chạy unit test không được cài Docker. Tuỳ hoàn cảnh, các bạn có thể thêm thuộc tính này nếu muốn nhé. Máy mình đang cài Docker nên mình ko cần phải khai báo thuộc tính này.
Bây giờ chúng ta sẽ khai báo container mà chúng ta sẽ cần chạy trước khi code unit test được execute với annotation @Container. Cho ví dụ của mình như sau:
1 2 3 4 5 6 |
@Container public static MySQLContainer<?> container = new MySQLContainer<>("mysql:latest") .withDatabaseName("example") .withPassword("123456") .withInitScript("db.sql") .withReuse(true); |
Mặc định thì container của MySQL với Testcontainer sẽ chạy với user “root” và password là “test”. Cho ví dụ của mình thì mình đã thay đổi password sang “123456” như các bạn thấy. Database name mình để là “example”.
Mình cũng khai báo một initial SQL script để khi chạy container lên, Testcontainers sẽ tự động tạo mới table students và insert data vào table này. Nội dung của tập tin db.sql nằm trong thư mục src/test/resources như sau:
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' |
Phương thức withReuse() giúp chúng ta có thể sử dụng lại container mà chúng ta đã khởi tạo trước đó.
Để chạy container mà chúng ta đã khai báo ở trên, các bạn có thể gọi phương thức start() trong phương thức được annotate với annotation @BeforeAll của JUnit 5 như sau:
1 2 3 4 5 6 7 |
@BeforeAll public static void init() throws SQLException { container.start(); connection = DriverManager.getConnection(container.getJdbcUrl(), "root", "123456"); } |
Như các bạn thấy, mình cũng đã khởi tạo đối tượng Connection từ thông tin của MySQL container mà mình đã chạy.
Bây giờ, thì các bạn có thể viết unit test cho phương thức getAllStudents() như sau:
1 2 3 4 5 |
@Test public void testGetAllStudents() throws SQLException { List<Student> allStudents = studentService.getAllStudents(connection); Assertions.assertTrue(allStudents.size() == 2); } |
Toàn bộ code unit test của mình cho class StudentService như sau:
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); } } |
Kết quả: