VieTopik
Tiếng HànTiếng AnhIT
  • Góc học tập
Tải app
  • Thư viện
  • Luyện thi
  • Cẩm nang
  • Góc học tập
Spring Boot Web APIHoàn thiện API
Bài 19/19
5 phút

Viết test cho API

Nội dung bài · 5 mục
  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 5.Tóm tắt

Bạn sửa công thức giảm giá, gọi thử bằng curl thấy đúng. Ba tuần sau, một người khác sửa code chỗ khác và làm hỏng công thức đó, nhưng không ai phát hiện. Test tự động giúp chạy lại mọi phép kiểm tra chỉ bằng một lệnh.

Khái niệm

🧪 Unit test: code tự động kiểm tra một phần nhỏ như một method hay một class có chạy đúng không.

✔️ JUnit: thư viện test phổ biến nhất của Java, mỗi method test đánh dấu @Test và kiểm tra kết quả bằng các method như assertEquals.

Mỗi test gồm ba bước:

Bước Làm gì
Arrange chuẩn bị object và dữ liệu
Act gọi method cần test
Assert kiểm tra kết quả đúng như mong đợi

Ví dụ

Class cần test nằm trong src/main/java/com/shop/api/:

// PriceCalculator.java
package com.shop.api;

class PriceCalculator {
    public long total(long price, int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException(
                "Số lượng phải > 0");
        }

        long total = price * quantity;
        if (total >= 500000) {
            total = total * 90 / 100;
        }
        return total;
    }
}

Project Spring Boot đã có sẵn JUnit và thư mục test src/test/java/com/shop/api/. Tạo file test trong thư mục đó rồi chạy:

./mvnw test
// PriceCalculatorTests.java
package com.shop.api;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.*;

class PriceCalculatorTests {
    @Test
    public void totalUnder500kNoDiscount() {
        var calculator = new PriceCalculator();

        long total = calculator.total(100000, 2);

        assertEquals(200000, total);
    }

    @Test
    public void totalZeroQuantityThrows() {
        var calculator = new PriceCalculator();

        assertThrows(IllegalArgumentException.class,
            () -> calculator.total(100000, 0));
    }
}
  • File test ghi cùng package com.shop.api; với class cần test, nên dùng được PriceCalculator dù class này không public.
  • import static cho gọi thẳng assertEquals mà không cần ghi Assertions. phía trước.
  • Mỗi test là một method @Test, tên nói rõ tình huống và kết quả mong đợi.
  • assertEquals(mong đợi, thực tế): hai giá trị khác nhau thì test đỏ.
  • assertThrows kiểm tra method có ném đúng loại exception không. () -> ... là lambda không tham số, bọc lời gọi để assertThrows tự chạy và bắt exception.

Thử ngay

Tạo thêm class test này trong src/test/java/com/shop/api/ rồi chạy lại ./mvnw test:

// BoundaryTests.java
package com.shop.api;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.*;

class BoundaryTests {
    @Test
    public void totalExactly500k() {
        var calculator = new PriceCalculator();

        long total = calculator.total(250000, 2);

        assertEquals(500000, total);
    }
}

Đoán trước khi chạy: test mới xanh hay đỏ?

Xem kết quả
[ERROR] Failures:
[ERROR]   BoundaryTests.totalExactly500k:14 expected: <500000> but was: <450000>
[ERROR] Tests run: 4, Failures: 1, Errors: 0, Skipped: 0
[INFO] BUILD FAILURE

Đỏ. Tổng đúng bằng 500000 thì điều kiện >= 500000 vẫn giảm 10%, còn 450000. Tests run là 4 vì project tạo từ start.spring.io có sẵn một test mẫu ShopApiApplicationTests.

Test ở mốc ranh giới như thế này bắt được những hiểu nhầm mà thử bằng tay dễ bỏ qua.

Lỗi hay gặp

Test phụ thuộc file, database hay API thật. Test chạy chậm, lúc xanh lúc đỏ tuỳ máy và mạng. Class cần những thứ đó thì nhận interface qua constructor, lúc test truyền vào một bản giả, như bài DIP ở khoá OOP.

// SAI — test phụ thuộc file thật trên máy
// ReportTests.java
package com.shop.api;

import static org.junit.jupiter.api.Assertions.*;

import java.io.IOException;
import java.nio.file.*;
import org.junit.jupiter.api.*;

class ReportTests {
    @Test
    public void readReport() throws IOException {
        String text = Files.readString(
            Path.of("C:/data/report.txt"));
        assertFalse(text.isEmpty());
    }
}

ReportSource nằm trong src/main, hai file còn lại nằm trong src/test:

// ĐÚNG — bản giả trả dữ liệu định sẵn, không đọc file
// ReportSource.java
package com.shop.api;

interface ReportSource {
    String read();
}

// FakeReportSource.java
package com.shop.api;

class FakeReportSource implements ReportSource {
    @Override
    public String read() {
        return "Doanh thu: 500000";
    }
}

// ReportTests.java
package com.shop.api;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.*;

class ReportTests {
    @Test
    public void readReport() {
        ReportSource source = new FakeReportSource();
        assertFalse(source.read().isEmpty());
    }
}

Một test kiểm tra quá nhiều thứ. Test đỏ mà không biết chỗ nào hỏng. Mỗi test nên kiểm tra một tình huống.

Tóm tắt

  • Unit test kiểm tra tự động một phần nhỏ của code.
  • Viết theo ba bước Arrange, Act, Assert, đánh dấu @Test.
  • ./mvnw test chạy toàn bộ test chỉ bằng một lệnh.
  • Test cả mốc ranh giới, và thay database hay API bằng bản giả.
  • Test controller qua HTTP mà không cần chạy server thì dùng MockMvc của Spring.

Tự kiểm tra

0/3 câu
Câu 1

Trong test, dòng long result = calculator.total(100, 3); thuộc bước nào?

Câu 2

File test mới trong src/test/java/com/shop/api/ mở đầu ngay bằng các dòng import. Test gọi new ShippingCalculator() thì báo lỗi compile không tìm thấy tên này. Thiếu gì?

Câu 3

Test viết assertEquals(total, 200000), trong đó total là kết quả thực tế. Khi test đỏ, thông báo lỗi sẽ thế nào?

Xác thực bằng JWT

Nội dung bài

  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 5.Tóm tắt