- vừa được xem lúc

SpringBoot Integration Test với Testcontainers

0 0 20

Người đăng: Blue Screen of Death

Theo Viblo Asia

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 !!!

Bình luận

Bài viết tương tự

- vừa được xem lúc

Tổng hợp các bài hướng dẫn về Design Pattern - 23 mẫu cơ bản của GoF

Link bài viết gốc: https://gpcoder.com/4164-gioi-thieu-design-patterns/. Design Patterns là gì. Design Patterns không phải là ngôn ngữ cụ thể nào cả.

0 0 302

- vừa được xem lúc

Học Spring Boot bắt đầu từ đâu?

1. Giới thiệu Spring Boot. 1.1.

0 0 278

- vừa được xem lúc

Cần chuẩn bị gì để bắt đầu học Java

Cần chuẩn bị những gì để bắt đầu lập trình Java. 1.1. Cài JDK hay JRE.

0 0 50

- vừa được xem lúc

Sử dụng ModelMapper trong Spring Boot

Bài hôm nay sẽ là cách sử dụng thư viện ModelMapper để mapping qua lại giữa các object trong Spring nhé. Trang chủ của ModelMapper đây http://modelmapper.org/, đọc rất dễ hiểu dành cho các bạn muốn tìm hiểu sâu hơn. 1.

0 0 194

- vừa được xem lúc

[Java] 1 vài tip nhỏ khi sử dụng String hoặc Collection part 1

. Hello các bạn, hôm nay mình sẽ chia sẻ về mẹo check String null hay full space một cách tiện lợi. Mình sẽ sử dụng thư viện Lớp StringUtils download file jar để import vào thư viện tại (link).

0 0 71

- vừa được xem lúc

Deep Learning với Java - Tại sao không?

Muốn tìm hiểu về Machine Learning / Deep Learning nhưng với background là Java thì sẽ như thế nào và bắt đầu từ đâu? Để tìm được câu trả lời, hãy đọc bài viết này - có thể kỹ năng Java vốn có sẽ giúp bạn có những chuyến phiêu lưu thú vị. DJL là tên viết tắt của Deep Java Library - một thư viện mã ng

0 0 139