+2

SpringBoot Integration Test với Testcontainers

Một trong những phần khó nhất của Integration Test là giả lập tương tác với các hệ thống bên thứ ba ví dụ như Database, HTTP Servers, ... và Caches. Tưởng tượng bạn đang sử dụng database PostgreSQL với ứng dụng SpringBoot, trong khi chạy integration test trên CI/CD pipeline chúng ta lại thường sử dụng in-memory database như H2. Việc sử dụng H2 có một vài nhược điểm sau:

  1. Vì H2 không phải là production database nên không mang lại độ tin cậy cao
  2. Nếu bạn đang viết bất kỳ native query nào sử dụng production database, bạn sẽ không thể test nó

Chúng ta có thể giải quyết các vấn đề trên bằng cách sử dụng thư viện java Testcontainers

Testcontainers là một thư viện java hỗ trợ Junit Tests, cung cấp các phiên bản lightweight, throwaway của các common instance như database, web browser hoặc bất kỳ thứ gì có thể chạy trên Docker container.

Testcontainers có thể giúp các loại test sau trở lên dễ dàng hơn:

  • Data access layer integration tests
  • Application integration tests
  • UI/Acceptance tests

Trong bài viết này tôi sẽ hướng dẫn cho các bạn cách đơn giản hóa việc integration test khi database sử dụng testcontainers.

1. Required Software

  • JDK 1.8+
  • JUnit 5
  • Docker
  • Springboot

Note : Testconatiners cũng hỗ trợ cả Junit 4.

2. Thêm Testcontainers dependency

Bạn có thể thêm phụ thuộc testcontainers theo hai cách sau:

2.1. Sử dụng BOM để thêm phụ thuộc vào file pom.xml

Lợi ích của việc sử dụng BOM là tránh được việc phải chỉ định version cụ thể cho từng phụ thuộc

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	......
	<properties>
		<java.version>11</java.version>
		<testcontainers.version>1.16.2</testcontainers.version>
	</properties>
	<dependencies>
		
		.....
		<dependency>
			<groupId>org.testcontainers</groupId>
			<artifactId>junit-jupiter</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.testcontainers</groupId>
			<artifactId>postgresql</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.testcontainers</groupId>
				<artifactId>testcontainers-bom</artifactId>
				<version>${testcontainers.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	....

</project>

2.2. Thêm phụ thuộc trực tiếp

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.16.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.16.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.16.2</version>
    <scope>test</scope>
</dependency>

Nếu bạn đang sử dụng PostgreSQL trong ứng dụng của mình, bạn cũng sẽ cần thêm phụ thuộc của JDBC driver vào file pom

<dependency>
	<groupId>org.postgresql</groupId>
	<artifactId>postgresql</artifactId>
	<scope>runtime</scope>
</dependency>

3. Sử dụng Testcontainers trong Integration Test

Chúng ta có thể khởi động các container sử dụng Testcontainers theo 3 cách:

  1. Sử dụng Special JDBC URL (Cái này chỉ dành riêng cho database container)
  2. Sử dụng annotation @Container (JUnit 5) hoặc @ClassRule (JUnit 4)
  3. Manual container starting

3.1. Sử dụng Special JDBC URL

Điều này chỉ dành riêng cho cơ sở dữ liệu, nếu bạn đang sử dụng thêm những container khác thì tốt hơn nên sử dụng cách 2 hoặc 3

Bắt đầu với một vài integration test cases:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers
public  class BaseIT {
	
	@Autowired
	protected TestRestTemplate testRestTemplate ;

}
public class DepartmentControllerIT extends BaseIT {



    @Test
    @Sql({ "/import.sql" })
    public void testGetDepartmentById() {

        ResponseEntity<Department> response = testRestTemplate.getForEntity( "/department/{id}",Department.class,100);
        Department dept =  response.getBody();

        assertEquals(100,dept.getId());
        assertEquals("HR", dept.getName());

    }

}
public class EmployeeControllerIT extends BaseIT {

    @Test
    @Sql({ "/import.sql" })
    public void testCreateEmployee() {

        Department dept = new Department();
        dept.setId(100);

        Employee emp = new Employee();

        emp.setFirst_name("abc");
        emp.setLast_name("xyz");
        emp.setDepartment(dept);
        emp.setBirth_date(LocalDate.of(1980,11,11));
        emp.setHire_date(LocalDate.of(2020,01,01));
        emp.setGender(Gender.F);

        ResponseEntity<Employee> response = testRestTemplate.postForEntity( "/employee", emp, Employee.class);

        Employee employee =  response.getBody();

        assertNotNull(employee.getId());
        assertEquals("abc", employee.getFirst_name());

    }

    @Test
    @Sql({ "/import.sql" })
    public void testGetEmployeeById() {

        ResponseEntity<Employee> response = testRestTemplate.getForEntity( "/employee/{id}",Employee.class,100);
        Employee employee =  response.getBody();

        assertEquals(100,employee.getId());
        assertEquals("Alex", employee.getFirst_name());

    }


}

Bạn có thể khởi tạo một database container mà không cần bất kỳ dòng code nào với special JDBC url

Bạn cần thêm vào file application.properties trong thư mục test resources các thuộc tính sau:

spring.datasource.url=jdbc:tc:postgresql:13.2:////<DatabaseName>
spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver

Nếu bạn cần run init script trong khi khởi động database bạn có thể thêm url như sau:

spring.datasource.url=jdbc:tc:postgresql:13.2:////<DatabaseName>?TC_INITSCRIPT=somepath/init_script.sql

TH scripts nằm trong đường dẫn file

spring.datasource.url=jdbc:tc:postgresql:13.2:////<DatabaseName>?TC_INITSCRIPT=file:src/main/resources/init_script.sql

Với MySQL bạn có thể sử dụng URL sau

jdbc:tc:mysql:5.7.34:///databasename

Giờ hãy chạy integration test với command sau:

mvn clean verify

3.2. Sử dụng @Container

Để sử dụng annotation Container bạn cần thêm chú tích @Testcontainers vào test class.

Thêm đoạn code sau vào test class:

@Container
public PostgreSQLContainer<?> postgresDB = new PostgreSQLContainer<>("postgres:13.2").withDatabaseName("eis");

Nếu đang sử dụng JUnit4

@ClassRule
public PostgreSQLContainer<?> postgresDB = new PostgreSQLContainer<>("postgres:13.2").withDatabaseName("eis");

Bạn cũng có tạo một lớp base test để tránh việc phải thêm các đoạn khởi tạo container vào tất cả các test class:

Chú ý: đoạn code bên dưới chỉ áp dùng cho Junit 5 và springboot version >= 2.2.6

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers
@DirtiesContext
public  class BaseIT {
	
	@Autowired
	protected TestRestTemplate testRestTemplate ;

	@Container
	public static PostgreSQLContainer<?> postgresDB = new PostgreSQLContainer<>                
                                                             ("postgres:13.2")
			                                     .withDatabaseName("eis")												                        
                                                             .withUsername("postgres")
                                                             .withPassword("postgres")
			                                     .withInitScript("ddl.sql");


	@DynamicPropertySource
	public static void properties(DynamicPropertyRegistry registry) {
		registry.add("spring.datasource.url",postgresDB::getJdbcUrl);
		registry.add("spring.datasource.username", postgresDB::getUsername);
		registry.add("spring.datasource.password", postgresDB::getPassword);

	}


}

Cùng phân tích đoạn code trên:

@SpringBootTest - annotation @SpringBootTest nói với spring boot tìm kiếm main configuration class và sử dụng class đó để khởi động spring application context

@Testcontainers - tìm tất cả các trường có @Container và gọi các phương thức vòng đời (prePost, preDestroy, ...) của chúng trong quá trình thực thi test

@DirtiesContext - đảm bảo mỗi subclass test sẽ có ApplicationContext riêng với các thuộc tính động chính xác

TestRestTemplate - giả lập cách tính huống call HTTP, một phiên bản mock của RestTemplate

@Container - sử dụng để đánh dấu container nên được quản lý bởi Testcontainer extension

Nếu đang sử dụng junit 5 và springboot version < 2.2.6 bạn có thể cấu hình như sau:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers
@ContextConfiguration(initializers = BaseIT2.TestEnvInitializer.class)
@DirtiesContext
public class BaseIT2 {

    @Autowired
    protected TestRestTemplate testRestTemplate ;


    @Container
    private static PostgreSQLContainer<?> postgresDB = new PostgreSQLContainer<>  
                                                             ("postgres:13.2")
                                                   .withDatabaseName("testdb")
                                                     .withUsername("postgres")
                                                     .withPassword("postgres")
                                                   .withInitScript("ddl.sql");



    static class TestEnvInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

        @Override
        public void initialize(ConfigurableApplicationContext applicationContext) {
            TestPropertyValues values = TestPropertyValues.of(
                    "spring.datasource.url=" + postgresDB.getJdbcUrl(),
                    "spring.datasource.password=" + postgresDB.getPassword(),
                    "spring.datasource.username=" + postgresDB.getUsername()
            );
            values.applyTo(applicationContext);

        }

    }

}

3.3. Manual container starting

Trong cách này chúng ta sẽ sử dụng singleton pattern để share container sang các test cases. Phương pháp này phù hợp để chạy một lượng lớn test cases nhưng cần cẩn thận để clear data giữa các testcases

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers
@DirtiesContext
public  class BaseIT {
	
	@Autowired
	protected TestRestTemplate testRestTemplate ;

	public static PostgreSQLContainer<?> postgresDB;

	static {
	 postgresDB = new PostgreSQLContainer<>("postgres:13.2")
			.withDatabaseName("eis");

	 postgresDB.start();
	}

	@DynamicPropertySource
	public static void properties(DynamicPropertyRegistry registry) {
		registry.add("spring.datasource.url",postgresDB::getJdbcUrl);
		registry.add("spring.datasource.username", postgresDB::getUsername);
		registry.add("spring.datasource.password", postgresDB::getPassword);

	}


}

Với cách này chúng ta sử dụng static block để define start container nên chúng sẽ trở nên khả dụng khi chương trình bắt đầu.

Hết o.O !!!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.