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

AnnotationChứ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ố.
@RepeatedTestChạy một test nhiều lần.
@TestFactoryTạo test động thay vì test tĩnh.
@TestTemplateDùng cho test được chạy nhiều lần theo context khác nhau.
@TestClassOrderQuy định thứ tự chạy các test class @Nested.
@TestMethodOrderQuy định thứ tự chạy test method trong class.
@TestInstanceXác định vòng đời của test instance.
@DisplayNameĐịnh nghĩa tên test case thân thiện hơn.
@DisplayNameGenerationTự động sinh tên test case.
@BeforeEachChạy trước mỗi test method (tương tự @Before trong JUnit 4).
@AfterEachChạy sau mỗi test method (tương tự @After trong JUnit 4).
@BeforeAllChạy trước tất cả test trong class (tương tự @BeforeClass).
@AfterAllChạy sau tất cả test trong class (tương tự @AfterClass).
@NestedDùng để tạo test class lồng nhau.
@TagGán nhãn test để filter khi chạy.
@DisabledVô hiệu hóa test method hoặc test class.
@TimeoutFail test nếu thời gian chạy vượt quá giới hạn.
@ExtendWithDù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@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@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@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.
  • 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.