Viết test cho API
Nội dung bài · 5 mục
- 1.Khái niệm
- 2.Ví dụ
- 3.Thử ngay
- 4.Lỗi hay gặp
- 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 đượcPriceCalculatordù class này khôngpublic. import staticcho gọi thẳngassertEqualsmà không cần ghiAssertions.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 đỏ.assertThrowskiểm tra method có ném đúng loại exception không.() -> ...là lambda không tham số, bọc lời gọi đểassertThrowstự 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 testchạ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âuTrong test, dòng long result = calculator.total(100, 3); thuộc bước nào?
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ì?
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?