Chặng cuối bắt đầu. Bài này đo các cách viết test trong Spring Boot, và tìm ra thứ thật sự làm chúng chậm.
Ba loại, ba con số
@SpringBootTest Started in 0,852 s | 191 bean
@WebMvcTest Started in 0,792 s | 132 bean
test đơn vị thuần không khởi động context | 0 bean
Lời khuyên phổ biến là "dùng lát cắt, nó nhanh hơn nhiều". Trên ứng dụng này, chênh lệch là 60 mili giây.
Lý do rất rõ khi nhìn số bean: 191 so với 132. Ứng dụng thử nghiệm của tôi chỉ có web và security — không có JPA, không có CSDL. Lát cắt chỉ tiết kiệm được thứ nó loại ra, mà ở đây gần như không có gì để loại.
Trên ứng dụng thật với JPA, Flyway, Redis và pool kết nối, khác biệt sẽ lớn hơn nhiều. Nhưng bài học là: đừng giả định, hãy đo trên chính dự án của bạn.
Thứ thật sự làm bộ test chậm
4 lớp test, CÙNG cấu hình : 2916 ms | context khởi động 1 lần
4 lớp test, MỖI lớp khác : 3422 ms | context khởi động 4 lần
Spring đệm lại ngữ cảnh test và dùng chung cho mọi lớp có cùng cấu hình. Khác cấu hình một chút là một ngữ cảnh mới.
Trên ứng dụng này, một ngữ cảnh khởi động trong 0,8 giây nên bốn cái chỉ tốn thêm nửa giây. Nhưng trên ứng dụng thật khởi động 5 giây, đó là 20 giây thay vì 5 — và bộ test có 30 cấu hình khác nhau thì bạn mất hai phút rưỡi chỉ để dựng ngữ cảnh.
Những thứ tạo ra ngữ cảnh mới:
@SpringBootTest(properties = "a=1") // mỗi bộ properties khác nhau
@ActiveProfiles("test-rieng") // mỗi tổ hợp profile
@MockBean DichVuNaoDo x; // mỗi tổ hợp @MockBean
@TestPropertySource(...)
@MockBean là thủ phạm hay gặp nhất: nó thay bean trong ngữ cảnh, nên mỗi tổ hợp @MockBean khác nhau là một ngữ cảnh riêng.
Ba cách giữ số ngữ cảnh nhỏ:
Gom cấu hình test vào một lớp cha rồi kế thừa. Mọi test dùng chung một ngữ cảnh.
Dùng @MockBean tiết kiệm. Cần giả lập thì cân nhắc tiêm một cài đặt giả qua cấu hình test dùng chung, thay vì @MockBean rải rác.
Đo số ngữ cảnh:
mvn test | grep -c "Started .* in .* seconds"
Con số đó lớn hơn số bạn nghĩ nhiều lần — tôi chưa gặp dự án nào không bất ngờ với nó.
Các lát cắt có sẵn
| Lát cắt | Nạp gì | Dùng cho |
|---|---|---|
@WebMvcTest |
controller, filter, MockMvc |
tầng web, có @MockBean cho service |
@DataJpaTest |
JPA, repository, CSDL nhúng | truy vấn (bài 36) |
@JsonTest |
Jackson | tuần tự hoá (bài 15) |
@RestClientTest |
RestClient, MockRestServiceServer |
gọi HTTP ra ngoài (bài 20) |
@DataRedisTest |
Redis | bài 37 |
@SpringBootTest |
tất cả | test tích hợp |
@WebMvcTest(DonController.class) chỉ nạp một controller — nhanh hơn và ít phụ thuộc hơn nữa.
@RestClientTest đáng dùng hơn nhiều người nghĩ: nó cho bạn giả lập dịch vụ ngoài ở mức HTTP thay vì giả lập client, nên bạn test được cả phần ánh xạ JSON và xử lý mã lỗi.
Kim tự tháp, và vì sao nó vẫn đúng
Nhiều test đơn vị. Không Spring, không context, chạy mili giây. Logic nghiệp vụ phải test được như vậy — nếu không thì đó là dấu hiệu logic đang dính chặt vào framework.
Vừa phải test lát cắt. Kiểm phần Spring làm hộ: ánh xạ request, truy vấn, tuần tự hoá.
Ít test đầu-cuối. Chậm, dễ chập chờn, nhưng chúng bắt được thứ hai tầng trên bỏ lọt — bài 47 vừa đo một trường hợp: MockMvc nói 403, máy chủ thật trả 405.
Sai lầm tôi thấy nhiều nhất là đảo ngược: mọi test đều là @SpringBootTest. Bộ test chạy mười lăm phút, không ai chạy nó trước khi đẩy mã, và giá trị của nó về gần không.
Viết logic để test được không cần Spring
// khó test: phụ thuộc trộn lẫn với logic
@Service class DvDon {
@Autowired RepoDon repo;
public BigDecimal tinhTien(Long id) {
var d = repo.findById(id).orElseThrow();
return d.getGia().multiply(...); // logic nằm chung với truy cập dữ liệu
}
}
// dễ test: logic là hàm thuần
class TinhTien {
static BigDecimal tinh(Don d, ChinhSach cs) { ... } // không Spring, không CSDL
}
Tách được như vậy thì phần lớn quy tắc nghiệp vụ test bằng JUnit thuần, chạy trong mili giây, và không có ngữ cảnh nào để đệm.
Đây cũng là nơi những bài về SOLID trả công.
Bốn điều đã gặp
MockMvc .param() cộng dồn giá trị chứ không ghi đè. Gọi hai lần cùng tên sẽ gửi hai giá trị và Spring nối chúng bằng dấu phẩy.
Test dùng @Transactional giữ dữ liệu chưa commit, nên thao tác chạy trên kết nối khác sẽ không nhìn thấy — đó là hành vi đúng, đừng "sửa" nó.
Cộng số test từ target/surefire-reports/ sẽ thiếu: lớp @Nested được ghi thành báo cáo riêng còn lớp cha hiện 0. Lấy con số tổng ở dòng cuối của mvn test.
Vài test cố tình sinh lỗi. Log có dòng ERROR là bình thường — miễn là bạn biết test nào sinh ra chúng.
@MockBean sắp bị thay
Từ Spring Boot 3.4, @MockBean và @SpyBean đã được đánh dấu ngừng dùng, thay bằng:
@MockitoBean DichVuNgoai dichVu;
@MockitoSpyBean DichVuThat that;
Cùng ý tưởng, nằm trong spring-test thay vì spring-boot-test. Dự án mới nên dùng tên mới ngay.
Thử ba mươi giây
mvn test | grep -c "Started .* in .* seconds"
Đó là số ngữ cảnh Spring mà bộ test của bạn dựng lên. Nhân nó với thời gian khởi động một ngữ cảnh, và bạn có phần lớn thời gian chạy test — cùng với danh sách chính xác thứ cần gom lại.
Ngày mai: cấu hình — profile, @ConfigurationProperties, và vì sao chuỗi rỗng không phải là "chưa cấu hình".