Một cái mock giống như bạn diễn tập gật gù đồng ý mọi câu: vở kịch nào cũng trơn tru trong phòng tập, rồi vỡ ngay đêm diễn thật. Hai mươi chín bài trước đầy những "trục trặc sân khấu" mà không mock nào tái hiện được: 406 khi khai lệch tham số, 541 với tổ hợp cờ bị cấm, x-death tăng dần, prefetch bị bỏ qua khi autoAck=true, vòng lặp requeue 11 189 lần. Nếu test của bạn dùng một RabbitTemplate giả, mọi thứ đó đều xanh. Bài này về cách chạy broker thật trong test mà không biến bộ test thành nửa tiếng chờ đợi.

Cấu trúc

@SpringBootTest
@Testcontainers
class DonHangTest {

    @Container
    @ServiceConnection                       // Spring Boot tự nối vào container này
    static final RabbitMQContainer RMQ = new RabbitMQContainer("rabbitmq:4");

    @Autowired RabbitTemplate tpl;
    @Autowired RabbitAdmin admin;

    @Test void thong_diep_khong_khop_binding_thi_mat() { ... }
}

Hai chi tiết quyết định thời gian chạy nằm ở dòng khai container: từ khoá static, và @Container đặt ở mức lớp. Cả hai làm cùng một việc — container sống suốt cả lớp test thay vì dựng lại cho từng phương thức.

Container dùng chung hay riêng

Đo bằng cách khởi động chính những container đó, ba "test" mỗi lần:

Thời gian
Một container dùng chung cả lớp 3,6 s
Mỗi test một container riêng 9,1 s

Chậm gấp 2,5 lần với chỉ ba test. Con số đó tuyến tính theo số test: một lớp 20 phương thức là khoảng 58 giây so với 5 giây — và đó là một lớp.

Bỏ static khỏi khai báo container là lỗi tốn thời gian nhất trong toàn bộ bài này, và nó không báo gì cả: test vẫn xanh, chỉ chậm. Sau vài tháng thì "bộ test RabbitMQ chạy lâu lắm" trở thành sự thật hiển nhiên mà không ai nhớ vì sao.

Muốn nhanh hơn nữa thì bật tái dùng container giữa các lần chạy (.withReuse(true) cộng testcontainers.reuse.enable=true trong ~/.testcontainers.properties) — hữu ích trên máy lập trình viên, nhưng nhớ dọn trạng thái ở đầu mỗi lớp vì hàng đợi cũ vẫn còn đó.

Ảnh gọn hơn không nhanh hơn

Lời khuyên hay gặp là dùng ảnh không có giao diện quản trị cho test. Đo thử:

Ảnh Tới khi nhận được kết nối
rabbitmq:4-management 2,8 s
rabbitmq:4 2,9 s

Không khác gì. Thời gian khởi động là thời gian node Erlang lên, không phải thời gian nạp plugin quản trị. Nên cứ dùng bản -management: khi một test hỏng khó hiểu, mở localhost:<port> xem hàng đợi đang có gì là cách gỡ nhanh nhất, và phần 5 cho thấy giao diện đó nói được nhiều thứ.

Đừng chờ bằng Thread.sleep

Cách viết phổ biến nhất trong test nhắn tin, và cũng tệ nhất:

tpl.convertAndSend("don-moi", don);
Thread.sleep(2000);              // "chắc là tới rồi"
assertThat(daNhan).isNotNull();

Năm phép chờ như vậy so với năm phép chờ bằng thư viện polling:

Cách chờ Thời gian 5 lần
Thread.sleep(2000) 10,11 s
Awaitility, poll mỗi 10 ms 0,12 s

Nhanh hơn 84 lần, và quan trọng hơn: nó không bị hỏng ngẫu nhiên. Thread.sleep(2000) vừa quá lâu cho trường hợp bình thường (thông điệp tới sau ~20 ms) vừa quá ngắn cho lần CI chạy chậm — đó là công thức của test đỏ ngẫu nhiên.

tpl.convertAndSend("don-moi", don);
await().atMost(Duration.ofSeconds(5))
       .untilAsserted(() -> assertThat(daNhan.get()).isNotNull());

Nên kiểm thử cái gì

Broker thật đắt hơn mock, nên đừng dùng nó để kiểm tra logic nghiệp vụ — thứ đó test đơn vị làm nhanh hơn. Dùng nó cho đúng những chỗ mà sê-ri này đo được là hay sai:

Topology có khớp không — hàng đợi, binding, routing key. Phần 8 đo được rằng sai một ký tự trong khoá là thông điệp biến mất không báo.

Đường lỗi có chạy không — ném ngoại lệ trong listener rồi kiểm tra thông điệp có sang nghĩa địa không. Phần 28 cho thấy hành vi này phụ thuộc cả vào loại ngoại lệ.

Chuyển đổi thông điệp — phần 26 đo được rằng __TypeId__ trói hai dịch vụ vào nhau; một test gửi–nhận thật bắt được chuyện đó, mock thì không.

Muốn kiểm ngay bộ test của mình có đang dựng thừa container không, đếm số lần container khởi động:

# xem test cua ban co dung chung container khong
docker events --filter 'event=start' --filter 'type=container' &
mvn test -Dtest=DonHangTest

Đếm số dòng start hiện ra. Nhiều hơn một cho một lớp test nghĩa là bạn quên static.

Mẫu số chung

Một mock chỉ diễn lại được những hành vi bạn đã biết trước để giả — nó là bạn diễn gật gù đồng ý mọi câu, nên về cấu trúc nó không thể sinh ra chính những cú hỏng nảy ra từ thứ thật (một 406, một __TypeId__ lệch, một cuộc đua thời gian). Điều đó lật ngược câu hỏi test cái gì ở đâu: mock cái bạn sở hữu và hiểu rõ (logic nghiệp vụ, nhanh), và dùng phụ thuộc thật ở cái ranh giới bạn không kiểm soát hết (broker, CSDL, trình duyệt) — vì những con bug đáng bắt ở đó đúng là những con mà một vật thế thân không tài nào diễn ra. Cùng khoảng hụt ở một CSDL trong-bộ-nhớ không hệt Postgres, một HTTP client giả không bao giờ hết giờ, một đồng hồ đóng băng giấu mất một lỗi lập lịch. Một bộ test mock xanh trọn vẹn là một sự an tâm, không phải một bằng chứng.

Điều thứ hai: một chi phí mỗi-đơn-vị nhân lên cả bộ test quyết định bộ test đó có được chạy hay không. Quên static (2,5 lần với ba test, tuyến tính về sau) và chờ bằng sleep thay vì poll (84 lần) đều là những cú thoái hoá vô hình — xanh, chỉ chậm — mà một bộ test chậm thì lặng lẽ bị bỏ không chạy, trạng thái tệ nhất một bộ test có thể rơi vào: còn đó, vẫn xanh, và không ai chạy. Hai cách chữa lại là hai cái quen thuộc — khấu hao cái đồ gá đắt (dùng chung container), và chờ tín hiệu thật thay vì một mốc thời gian mình đoán — nhưng cái giá ở đây sắc hơn: một bài test không ai chạy thì chẳng bảo vệ gì, và không có cờ đỏ nào báo cho bạn biết điều đó đã xảy ra.

Bài sau: trace id đi theo thông điệp, và vì sao ngữ cảnh truy vết dừng lại ngay tại basicPublish.