99 Ngày Spring — Ngày 54: @SpringBootTest
Ba ngày qua ta cắt ứng dụng thành từng lát: service, web, dữ liệu. Hôm nay ghép tất cả lại — @SpringBootTest nạp toàn bộ ứng dụng và gọi request qua HTTP thật. Nhưng câu hỏi thú vị không phải "làm thế nào" mà là "khi nào thì xứng đáng": mỗi lần nạp context tốn ba tới mười lăm giây, và dùng sai chỗ thì bộ test của bạn chạy bốn mươi phút thay vì hai phút.

Khi nào thì xứng đáng, và chọn webEnvironment nào
@SpringBootTest chỉ nên dành cho những gì test nhỏ không chứng minh được: cả luồng đặt hàng có chạy thông không, cấu hình có ghép đúng không, transaction có trải đúng qua nhiều service không. Logic nghiệp vụ vẫn ở unit test của Ngày 51 — kim tự tháp là nhiều unit test, ít slice test, rất ít test tích hợp.
// @SpringBootTest — nạp TOÀN BỘ ứng dụng. Câu hỏi không phải "làm thế nào",
// mà là "KHI NÀO thì xứng đáng".
@SpringBootTest(webEnvironment = RANDOM_PORT) // ⭐ dựng server thật trên cổng ngẫu nhiên
class DatHangIntegrationTest {
@Autowired TestRestTemplate rest; // client HTTP thật, tự trỏ đúng cổng
@Autowired DonHangRepository repo; // và bạn xem được DB để kiểm chứng
@Test
void dat_hang_thi_luu_don_va_tru_ton_kho() {
var res = rest.postForEntity("/api/don",
new TaoDonRequest("SP-01", 2), DonHangResponse.class);
assertThat(res.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(repo.findAll()).hasSize(1); // ⭐ đi qua CẢ ngăn xếp
assertThat(khoRepo.tonKho("SP-01")).isEqualTo(98); // và kiểm chứng ở tầng DB
}
}
// ⭐ BỐN LỰA CHỌN webEnvironment, và chọn sai là tự làm khổ mình:
// MOCK (mặc định) — KHÔNG có server, dùng MockMvc. Nhanh nhất.
// RANDOM_PORT — server THẬT, cổng ngẫu nhiên. ⭐ Dùng cái này cho test tích hợp:
// chạy song song thoải mái, không bao giờ đụng cổng trên CI.
// DEFINED_PORT — server thật, cổng 8080. ❌ Hai test song song là vỡ.
// NONE — không web gì cả, chỉ context.
// ❗ GIÁ PHẢI TRẢ, nói thẳng: 3–15 giây MỖI LẦN nạp context.
// ➜ Nên @SpringBootTest chỉ dành cho những gì test nhỏ KHÔNG chứng minh được:
// "cả luồng đặt hàng có chạy thông không", "cấu hình có ghép đúng không",
// "transaction có trải đúng qua nhiều service không".
// ➜ Logic nghiệp vụ vẫn là unit test (Ngày 51). Đừng đưa lên đây.
// Kim tự tháp: nhiều unit test, ít slice test, RẤT ít @SpringBootTest.RANDOM_PORTlà lựa chọn đúng cho test tích hợp — chạy song song thoải mái và không bao giờ đụng cổng trên CI, khác hẳnDEFINED_PORT.MOCKlà mặc định và nhanh nhất, nhưng nó dùngMockMvc— nghĩa là bạn đang làm lại việc của@WebMvcTestvới giá đắt hơn nhiều.Giá trị thật của test tích hợp là nó đi qua cả ngăn xếp rồi bạn kiểm chứng được ở tầng database.
Cache context — thứ quyết định 2 phút hay 20 phút
Đây là phần ít người biết mà ảnh hưởng lớn nhất. Spring cache lại context và tái sử dụng giữa các class test, với khóa cache là toàn bộ cấu hình. Khác một chút — thêm một @MockitoBean, đổi một property — là một context mới, cộng thêm mười lăm giây.
// CACHE CONTEXT — thứ quyết định bộ test của bạn chạy 2 phút hay 20 phút.
// ⭐ Spring CACHE LẠI context và TÁI SỬ DỤNG giữa các class test.
// Khóa cache là toàn bộ CẤU HÌNH: classes, profile, properties, mock bean...
// Cấu hình GIỐNG NHAU → dùng lại context cũ → nạp một lần cho cả trăm test.
// Cấu hình KHÁC một chút → nạp context MỚI → cộng thêm 3–15 giây.
// ❗ BỐN THỨ ÂM THẦM PHÁ CACHE — mỗi cái là một context mới:
@SpringBootTest @ActiveProfiles("test") // context A
@SpringBootTest @ActiveProfiles("test") @MockitoBean X x; // context B — khác A!
@SpringBootTest(properties = "a=1") // context C
@SpringBootTest @DirtiesContext // ⭐ HỦY cache, nạp lại
// ➜ Một dự án có 30 class test với 12 tổ hợp cấu hình khác nhau
// = 12 lần nạp context = vài phút chỉ để KHỞI ĐỘNG.
// ➜ MẸO THỰC DỤNG: gom cấu hình chung vào MỘT lớp cha rồi kế thừa:
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles("test")
abstract class BaseIntegrationTest { } // ⭐ mọi test kế thừa → MỘT context duy nhất
class DatHangTest extends BaseIntegrationTest { }
class ThanhToanTest extends BaseIntegrationTest { }
// ❗ @DirtiesContext là công cụ ĐẮT NHẤT trong bộ test: nó vứt cache đi.
// Chỉ dùng khi test thật sự làm hỏng trạng thái context (đổi bean, đóng tài nguyên).
// Nhiều người rắc nó khắp nơi "cho chắc" rồi thắc mắc sao CI chạy 40 phút.
// PROFILE TEST — tách cấu hình test khỏi cấu hình thật (Ngày 09):
// src/test/resources/application-test.yml
spring.jpa.hibernate.ddl-auto: create-drop
logging.level.org.hibernate.SQL: DEBUG
// ❗ ĐỪNG để test trỏ vào DB thật. Nghe hiển nhiên, nhưng chuyện này xảy ra
// thường xuyên hơn bạn tưởng — và ddl-auto: create-drop thì XÓA SẠCH bảng.Gom cấu hình chung vào một lớp cha rồi kế thừa — ba mươi class test dùng chung một context thay vì mười hai lần nạp.
@DirtiesContextlà công cụ đắt nhất trong bộ test: nó vứt cache đi. Nhiều người rắc nó khắp nơi "cho chắc" rồi thắc mắc sao CI chạy bốn mươi phút.Đừng để test trỏ vào DB thật — nghe hiển nhiên nhưng xảy ra thường xuyên hơn bạn tưởng, và
ddl-auto: create-dropthì xóa sạch bảng.
Client, và cái bẫy xác thực
TestRestTemplate gọn cho MVC truyền thống, WebTestClient có API fluent và dùng được cho cả hai — nhiều dự án mới chọn nó để đội chỉ phải học một cú pháp. Nhưng cái đáng nhớ nhất ở đây là một cái bẫy: @WithMockUser hoàn toàn vô nghĩa trong test tích hợp.
// TESTRESTTEMPLATE hay WEBTESTCLIENT — chọn theo API bạn xây, không theo mốt.
// ① TestRestTemplate — đơn giản, đồng bộ, hợp với Spring MVC truyền thống:
var res = rest.getForEntity("/api/don/1", DonHangResponse.class);
assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
// ⭐ Nó KHÔNG ném ngoại lệ khi gặp 4xx/5xx (khác RestTemplate thường) —
// nên bạn khẳng định được mã lỗi thay vì phải bọc try-catch.
var loi = rest.getForEntity("/api/don/999", String.class);
assertThat(loi.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); // ✅ gọn
// ② WebTestClient — API fluent, dùng được cho CẢ WebFlux lẫn MVC:
webTestClient.get().uri("/api/don/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.maSanPham").isEqualTo("SP-01");
// ⭐ Từ Spring 5, WebTestClient chạy được với MVC luôn, nên nhiều dự án mới
// dùng nó cho tất cả để đội chỉ phải học MỘT cú pháp.
// ❗ XÁC THỰC trong test tích hợp — @WithMockUser KHÔNG có tác dụng ở đây!
// Vì request đi qua HTTP THẬT, không phải gọi trong tiến trình như MockMvc.
rest.withBasicAuth("user", "pass").getForEntity(...); // ✅ đăng nhập thật
// hoặc lấy token thật rồi gắn vào header:
var headers = new HttpHeaders();
headers.setBearerAuth(layTokenThat());
// ⭐ Đây là khác biệt hay làm người ta mất cả buổi: cùng một annotation
// chạy tốt ở @WebMvcTest hôm kia nhưng vô nghĩa ở @SpringBootTest RANDOM_PORT.
// ❗ VÀ NHỚ: @Transactional trên test tích hợp có server thật thì KHÔNG rollback
// được thao tác của server, vì server chạy trên LUỒNG KHÁC với transaction của test.
// ➜ Phải tự dọn dữ liệu, hoặc dùng Testcontainers dựng DB mới mỗi lần (Ngày 55).Request đi qua HTTP thật, không phải gọi trong tiến trình như
MockMvc— nên phải đăng nhập thật hoặc gắn token thật vào header.Cùng một annotation chạy tốt ở
@WebMvcTesthôm kia mà vô dụng ở đây — khác biệt này làm người ta mất cả buổi.TestRestTemplatekhông ném ngoại lệ khi gặp 4xx/5xx, nên bạn khẳng định được mã lỗi thay vì bọctry-catch.@Transactionalkhông rollback được thao tác của server vì server chạy trên luồng khác — phải tự dọn, hoặc dựng DB mới mỗi lần (Ngày 55).
Bài tập nhỏ
Viết một test tích hợp với
RANDOM_PORTđi từ HTTP xuống tới kiểm chứng ở database.Thêm một
@MockitoBeanvào một class test rồi đo xem tổng thời gian chạy tăng bao nhiêu.Gom hai class test vào một lớp cha chung và so số lần nạp context trong log.
Thử
@WithMockUsertrên testRANDOM_PORTđể thấy nó không có tác dụng gì.Viết một test ghi dữ liệu qua HTTP rồi xem
@Transactionalcó rollback được không.
Kết lại
Bốn ý gói lại hôm nay: @SpringBootTest chỉ dành cho những gì test nhỏ không chứng minh được — kim tự tháp là nhiều unit test, rất ít test tích hợp; RANDOM_PORT là lựa chọn đúng vì nó không bao giờ đụng cổng trên CI; cache context quyết định bộ test chạy hai phút hay bốn mươi phút, nên hãy gom cấu hình vào một lớp cha và đừng rắc @DirtiesContext cho chắc; và @WithMockUser vô nghĩa ở đây vì request đi qua HTTP thật — phải đăng nhập thật. Ngày 55 khép lại năm ngày kiểm thử bằng Testcontainers: Postgres thật trong container, @ServiceConnection, và cách chạy trong CI. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


