Unit Test
pom.xml
<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 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>UnitTest</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>UnitTest</name>
<url>http://maven.apache.org</url>
<dependencies>
<!-- API để viết test -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.12.1</version>
<scope>test</scope>
</dependency>
<!-- Engine để chạy test -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.12.1</version>
</dependency>
<!-- Hỗ trợ test tham số hóa -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.12.1</version>
</dependency>
<!-- Thư viện commons-lang3 là một phần của Apache Commons, cung cấp nhiều tiện ích cho Java
giúp xử lý chuỗi (String), số (Number), mảng (Array), đối tượng (Object), thời gian (Date/Time)
một cách dễ dàng. -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.17.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<dependencies>
<dependency>
<groupId>me.fabriciorby</groupId>
<artifactId>maven-surefire-junit5-tree-reporter</artifactId>
<version>0.1.0</version>
</dependency>
</dependencies>
<configuration>
<reportFormat>plain</reportFormat>
<consoleOutputReporter>
<disable>true</disable>
</consoleOutputReporter>
<statelessTestsetInfoReporter
implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5StatelessTestsetInfoTreeReporter" />
</configuration>
</plugin>
</plugins>
</build>
</project>
- Chạy lệnh để xem có version mới cho dependencies không: mvn versions:display-dependency-updates
Simple example
package com.example;
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
}
// ------------------------------------------
package com.example;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
Calculator calc = new Calculator();
@Test
void testAdd() {
assertEquals(5, calc.add(2, 3)); // Kiểm tra 2 + 3 có bằng 5 không
}
@Test
void testSubtract() {
assertEquals(1, calc.subtract(3, 2)); // Kiểm tra 3 - 2 có bằng 1 không
}
}
- Chạy lệnh test: mvn test, thì maven sẽ thực hiện chạy trên tất cả các test cases trong /src/test/java/.
Annotation
| Annotation | Chức năng |
|---|---|
@Test | Đánh dấu một method là test case. |
@ParameterizedTest | Đánh dấu method là test với dữ liệu tham số. |
@RepeatedTest | Chạy một test nhiều lần. |
@TestFactory | Tạo test động thay vì test tĩnh. |
@TestTemplate | Dùng cho test được chạy nhiều lần theo context khác nhau. |
@TestClassOrder | Quy định thứ tự chạy các test class @Nested. |
@TestMethodOrder | Quy định thứ tự chạy test method trong class. |
@TestInstance | Xác định vòng đời của test instance. |
@DisplayName | Định nghĩa tên test case thân thiện hơn. |
@DisplayNameGeneration | Tự động sinh tên test case. |
@BeforeEach | Chạy trước mỗi test method (tương tự @Before trong JUnit 4). |
@AfterEach | Chạy sau mỗi test method (tương tự @After trong JUnit 4). |
@BeforeAll | Chạy trước tất cả test trong class (tương tự @BeforeClass). |
@AfterAll | Chạy sau tất cả test trong class (tương tự @AfterClass). |
@Nested | Dùng để tạo test class lồng nhau. |
@Tag | Gán nhãn test để filter khi chạy. |
@Disabled | Vô hiệu hóa test method hoặc test class. |
@Timeout | Fail test nếu thời gian chạy vượt quá giới hạn. |
@ExtendWith | Dùng để tích hợp extension vào JUnit. |
- Mặc định mỗi test method chạy trên một instance mới của test class
- Có thể cấu hình JUnit để chỉ tạo một instance duy nhất của test class, thay vì tạo một instance mới cho mỗi test method. Dùng @TestInstance(Lifecycle.PER_CLASS), JUnit chỉ tạo một instance duy nhất của test class,
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SampleTest {
//
}
@BeforeEach - @AfterEach
@BeforeEach
void setup() {
System.out.println("Chạy trước mỗi test!");
}
@AfterEach
void cleanup() {
System.out.println("Chạy sau mỗi test!");
}
@BeforeAll - @AfterAll
- JUnit 5 mặc định tạo một instance mới của test class cho mỗi test method, nếu một method không phải static, nó sẽ gắn với một instance cụ thể.
- Nhưng @BeforeAll và @AfterAll chỉ chạy một lần cho cả test class → JUnit không thể gán chúng vào một instance cụ thể nếu nó không static.
@BeforeAll
static void init() {
System.out.println("Chạy trước tất cả test!");
}
@AfterAll
static void tearDown() {
System.out.println("Chạy sau tất cả test!");
}
- Nếu không muốn dùng static cho @BeforeAll và @AfterAll, bạn có thể cấu hình JUnit để chỉ tạo một instance duy nhất của test class, thay vì tạo một instance mới cho mỗi test method.
- Dùng @TestInstance(Lifecycle.PER_CLASS), JUnit chỉ tạo một instance duy nhất của test class, giúp bạn bỏ static trong @BeforeAll và @AfterAll.
import org.junit.jupiter.api.TestInstance;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CalculatorTest {
@BeforeAll
void init() {
System.out.println("Chạy trước tất cả test!");
}
@AfterAll
void tearDown() {
System.out.println("Chạy sau tất cả test!");
}
}
- Việc dùng TestInstance hay không dùng, phụ thuộc vào việc có muốn giữ trạng thái giữa các test method không.;
@Disabled
@Disabled("Disabled until bug #99 has been fixed")
class DisabledClassDemo {
@Test
void testWillBeSkipped() {
}
}
// -------------------------------------------------
@Disabled("Disabled until bug #42 has been resolved")
@Test
void testWillBeSkipped() {
}
@RepeatedTest
@RepeatedTest(5)
void testAdd() {
assertEquals(5, calc.add(2, 3)); // Kiểm tra 2 + 3 có bằng 5 không
}
@ParameterizedTest
package com.example;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.apache.commons.lang3.StringUtils;
class CalculatorTest {
Calculator calc = new Calculator();
CalculatorTest() {
System.out.println("Khởi tạo một instance mới của CalculatorTest");
}
@ParameterizedTest
@ValueSource(strings = { "racecar", "radarr", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.reverse(candidate).equalsIgnoreCase(candidate),
"Chuỗi '" + candidate + "' không phải palindrome");
}
}
@Tag
-
Gán tag cho test để có thể chạy một nhóm test nhất định bằng Maven hoặc IntelliJ.
-
mvn test -Dgroups=fast
@Tag("fast")
@Test
void testAdd() {
assertEquals(4, calc.add(2, 2));
}
@Timeout
- Giới hạn thời gian chạy test, nếu quá thời gian thì test fail.
@Test
@Timeout(value = 2, unit = TimeUnit.SECONDS)
void testTimeout() throws InterruptedException {
Thread.sleep(3000); // Test này sẽ fail vì mất 3s để chạy
}
@TestMethodOrder
- Xác định thứ tự chạy của các phương thức test trong cùng một class.
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.TestMethodOrder;
@TestMethodOrder(OrderAnnotation.class)
class CalculatorTest {
@Order(3)
@Test
void palindromes(String candidate) {
//
}
@Order(1)
@Test
void testAdd() {
//
}
@Order(2)
@Test
void testTimeout() throws InterruptedException {
//
}
}
@TestClassOrder
- Sắp xếp các class con theo thứ tự @Order
import org.junit.jupiter.api.*;
@TestClassOrder(ClassOrderer.OrderAnnotation.class) // ✅ Sắp xếp class con theo @Order
class ParentTest {
@Nested
@Order(2)
class SecondTest {
@Test
void testB() {
System.out.println("Test B - chạy thứ hai");
}
}
@Nested
@Order(1)
class FirstTest {
@Test
void testA() {
System.out.println("Test A - chạy đầu tiên");
}
}
}
@TestFactory
-
@TestFactory dùng để tạo test động (Dynamic Test) thay vì test cố định.
-
Tạo test dựa vào giá trị động, phụ thuộc vào runtime, tức là test sẽ không cố định
-
Các phương thức phải trả về Collection
hoặc Stream . - Collection
- Là một tập hợp (list, set, v.v.) chứa nhiều DynamicTest.
- Dùng khi số lượng test đã biết trước và không cần xử lý theo luồng (stream).
- Stream
: - Dùng khi số lượng test chưa biết trước (lấy từ API, file, database, v.v.).
- Dùng .map() để biến đổi dữ liệu thành test động.
- Collection
-
Test API lấy danh sách user từ server
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.DynamicTest;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
class ApiTest {
// Giả sử đây là API call thực tế (thay bằng logic thực tế của bạn)
List<String> getUsersFromApi() {
return List.of("alice", "bob", "charlie"); // Dữ liệu có thể thay đổi mỗi lần chạy
}
@TestFactory
Stream<DynamicTest> testUserApi() {
List<String> users = getUsersFromApi();
return users.stream()
.map(user -> DynamicTest.dynamicTest("Kiểm tra user: " + user,
() -> assertTrue(user.length() > 2, "User quá ngắn: " + user)));
}
}
- Test với dữ liệu từ Database
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.DynamicTest;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
class DatabaseTest {
// Giả sử đây là phương thức lấy dữ liệu từ DB (thay bằng truy vấn thực tế)
List<String> getUsernamesFromDatabase() {
return List.of("admin", "guest", "root"); // Dữ liệu có thể thay đổi mỗi lần test
}
@TestFactory
Stream<DynamicTest> validateDatabaseUsers() {
List<String> usernames = getUsernamesFromDatabase();
return usernames.stream()
.map(username -> DynamicTest.dynamicTest("Kiểm tra username: " + username,
() -> assertFalse(username.isEmpty(), "Username không được rỗng")));
}
}
@TestTemplate
- @TestTemplate không phải là một test đơn lẻ, mà là một mẫu (template) để chạy nhiều test động.
- Không tự chạy, mà cần một Provider (TestTemplateInvocationContextProvider) để quyết định số lần chạy và dữ liệu nào sẽ dùng trong mỗi lần chạy.
package com.example;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertTrue;
class FruitTest {
// Danh sách trái cây hợp lệ
private final List<String> fruits = Arrays.asList("apple", "banana", "lemon");
@TestTemplate
@ExtendWith(MyTestTemplateInvocationContextProvider.class)
void testTemplate(String fruit) {
System.out.println("🔍 Kiểm tra: " + fruit);
assertTrue(fruits.contains(fruit), "⚠️ Lỗi: " + fruit + " không có trong danh sách!");
}
}
// Provider cung cấp dữ liệu test
// Đây là một JUnit 5 Extension dùng để tạo test động.
// Xác định test chạy bao nhiêu lần và dữ liệu test là gì
class MyTestTemplateInvocationContextProvider implements TestTemplateInvocationContextProvider {
// Xác nhận test có thể chạy
// Cho phép JUnit chạy @TestTemplate.
@Override
public boolean supportsTestTemplate(ExtensionContext context) {
return true;
}
// Quyết định số lần chạy test
@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
ExtensionContext context) {
return Stream.of(invocationContext("apple"), invocationContext("banana"));
}
// Định nghĩa test context
// Tạo một lần chạy test với giá trị parameter (apple hoặc banana).
private TestTemplateInvocationContext invocationContext(String parameter) {
return new TestTemplateInvocationContext() {
// JUnit sẽ gọi getDisplayName để lấy tên cho mỗi lần chạy của test.
@Override
public String getDisplayName(int invocationIndex) {
return parameter; // Đặt tên test theo loại trái cây
}
// JUnit gọi getAdditionalExtensions() để lấy các extension cần thiết.
// Vì testTemplate(String fruit) có một tham số String fruit.
// JUnit cần biết làm sao để truyền giá trị vào tham số fruit.
// ParameterResolver sẽ giúp inject giá trị vào tham số fruit.
@Override
public List<Extension> getAdditionalExtensions() {
return Collections.singletonList(new ParameterResolver() {
// Kiểm tra xem test có cần tham số kiểu String không.
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return parameterContext.getParameter().getType().equals(String.class);
}
// Trả về parameter ("apple", "banana") để inject vào test.
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return parameter; // Truyền giá trị cho test
}
});
}
};
}
}
@DisplayName
- Không có displayName: nó show method name
- plugin
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] |
[INFO] +-- com.example.CalculatorTest
[INFO] | +-- [OK] testAdd - 0.037 ss
[INFO] | +-- [OK] testAddWithNegativeNumbers - 0.001 ss
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.572 s
[INFO] Finished at: 2025-03-20T14:52:28+07:00
[INFO] ------------------------------------------------------------------------
- Có thì:
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] |
[INFO] +-- com.example.CalculatorTest
[INFO] | +-- [OK] My Test Case 1 - 0.029 ss
[INFO] | +-- [OK] My Test Case 2 - 0.002 ss
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.634 s
[INFO] Finished at: 2025-03-20T14:53:30+07:00
[INFO] ------------------------------------------------------------------------
@DisplayNameGeneration
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class CalculatorTest {
@Test
void test_addition() {
// Đây sẽ tự động trở thành "test addition"
}
@Test
void test_subtraction() {
// Đây sẽ tự động trở thành "test subtraction"
}
}
- Giúp thay thế các dấu gạch dưới (_) trong tên method bằng dấu cách ( ).
- Như vậy, tên method test_addition() sẽ được hiển thị là "test addition".
@Nested
- @Nested được dùng để nhóm các test case vào trong các class con.
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
class CalculatorTest {
@Nested
class AdditionTests {
@Test
void test_addition() {
int result = 1 + 1;
assert(result == 2);
}
}
@Nested
class SubtractionTests {
@Test
void test_subtraction() {
int result = 2 - 1;
assert(result == 1);
}
}
}
@AutoClose
- Giúp tự động đóng tài nguyên khi test hoàn thành, tránh rò rỉ tài nguyên.
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.AutoClose;
import static org.junit.jupiter.api.Assertions.*;
class AutoCloseDemo {
@AutoClose
WebClient webClient = new WebClient(); // WebClient sẽ được tự động đóng
String serverUrl = "http://example.com"; // Địa chỉ của server
@Test
void getProductList() {
// Dùng WebClient để kết nối với server và kiểm tra phản hồi
assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
}
}
@TempDir
- Annotation này tạo thư mục tạm thời (temporary directory) dành riêng cho mỗi test. Dùng để tạo file tạm mà không cần tự quản lý.
@Test
void writeItemsToFile(@TempDir Path tempDir) throws IOException {
Path file = tempDir.resolve("test.txt");
new ListWriter(file).write("a", "b", "c");
assertEquals(singletonList("a,b,c"), Files.readAllLines(file));
}
@ExtendWith
- Annotation này mở rộng khả năng của JUnit bằng cách cho phép sử dụng extensions (các tiện ích mở rộng như mock, logging, hay rule).
@RegisterExtension
- RegisterExtension nó chỉ apply cho những instance của các test cụ thể chứ không apply cho toàn bộ class với tất cả các test.