Viết Integration Tests Cô Lập với TestContainers

Xuất bản ngày 28 tháng 8, 2025

Tác giả: Tim Deschryver

Tại sao TestContainers lại quan trọng?

Có hai loại kiểm thử chính: unit tests và integration tests. Unit tests nhỏ, nhanh và cô lập. Chúng kiểm tra một đơn vị mã duy nhất, thường là một hàm hoặc phương thức, tách biệt với phần còn lại của hệ thống. Integration tests ngược lại kiểm tra cách các phần khác nhau của hệ thống hoạt động cùng nhau. Chúng thường lớn hơn và có thể chậm hơn unit tests. Vì integration tests bao phủ nhiều hơn, chúng yêu cầu quá trình thiết lập phức tạp hơn, đây là rào cản mà bạn phải vượt qua.

Tuy nhiên, tôi thích viết integration tests hơn unit tests vì chúng cho tôi nhiều sự tin tưởng hơn rằng hệ thống hoạt động như mong đợi. Đó là lý do tại sao tôi muốn cho bạn thấy rằng viết integration tests không phải lúc nào cũng là một nhiệm vụ phức tạp và tẻ nhạt.

Hầu hết các ứng dụng đều tương tác với các hệ thống bên ngoài như cơ sở dữ liệu, SMTP clients, message brokers và API của bên thứ ba. Điều này là cần thiết và ổn, nhưng bạn không muốn các bài kiểm thử của mình phụ thuộc vào các hệ thống bên ngoài này vì điều đó sẽ làm cho chúng chậm, không ổn định và nguy hiểm.

Để ngăn chặn điều này, bạn muốn các bài kiểm thử của mình được cô lập khỏi các hệ thống bên ngoài này, điều này có thể được thực hiện bằng nhiều cách. Bạn có thể tạo mocks hoặc sử dụng các gói cung cấp một triển khai khác của hệ thống bên ngoài (ví dụ: cơ sở dữ liệu trong bộ nhớ). Nhưng bằng cách đó, bạn không kiểm tra tích hợp thực tế với hệ thống bên ngoài thực sự. Tệ hơn, bạn có thể gặp phải các vấn đề vì mock hoạt động khác với instance thực.

Một cách tiếp cận khác là sử dụng một instance kiểm thử trực tiếp mà tất cả các bài kiểm thử kết nối đến và thay thế kết nối đến instance kiểm thử trong quá trình kiểm thử. Tuy nhiên, vì các bài kiểm thử (và nhà phát triển và pipeline) chia sẻ một instance trực tiếp, điều này không lý tưởng vì nó có thể dẫn đến các vấn đề như các bài kiểm thử can thiệp lẫn nhau. Cách tiếp cận này không áp dụng được cho tất cả các hệ thống bên ngoài.

TestContainers là giải pháp tốt hơn

Từ trang web TestContainers, đây là cách họ mô tả về chính họ:

Testcontainers là một thư viện mã nguồn mở cung cấp các instance nhẹ, có thể vứt bỏ của cơ sở dữ liệu, message brokers, trình duyệt web, hoặc hầu như bất cứ thứ gì có thể chạy trong container Docker. Không cần mocks hoặc cấu hình môi trường phức tạp. Định nghĩa các phụ thuộc kiểm thử của bạn dưới dạng mã, sau đó chỉ cần chạy các bài kiểm thử của bạn và các container sẽ được tạo và sau đó xóa.

Phần quan trọng ở đây là cơ sở hạ tầng được định nghĩa dưới dạng mã, điều này làm cho việc thiết lập và dọn dẹp cơ sở hạ tầng cần thiết cho các bài kiểm thử của bạn trở nên dễ dàng. Nếu không có TestContainers, đây sẽ là một nhiệm vụ tẻ nhạt.

Điều này mang lại cho bạn nhiều lợi ích, mà không có nhược điểm của các cách tiếp cận khác. Giá trị lớn nhất đối với tôi là tôi có thể chạy các bài kiểm thử của mình chống lại một instance thực, đảm bảo rằng tích hợp hoạt động như mong đợi.

Các lợi ích khác:

  • Cô lập: Mỗi bài kiểm thử có thể có container riêng của nó, đảm bảo rằng các bài kiểm thử hoặc người không can thiệp lẫn nhau.
  • Tính nhất quán: Các bài kiểm thử luôn chạy trong cùng một môi trường, được thiết lập cho mỗi lần chạy.
  • Đơn giản: Không cần tạo và duy trì mocks.

Điều kiện tiên quyết

Để sử dụng TestContainers, yêu cầu duy nhất là máy của bạn đã cài đặt và đang chạy Docker.

Cài đặt TestContainers

TestContainers có sẵn cho nhiều ngôn ngữ lập trình và cung cấp container cho nhiều hệ thống bên ngoài phổ biến. Trong bài đăng blog này, chúng ta sẽ giữ nó đơn giản với chỉ một cơ sở dữ liệu PostgreSQL.

Để cài đặt gói TestContainers PostgreSQL, chạy lệnh sau trong dự án của bạn:

dotnet add package Testcontainers.PostgreSql

Tạo một container kiểm thử

Bây giờ chúng ta đã cài đặt gói, chúng ta có thể thiết lập một container PostgreSQL trong các bài kiểm thử của mình, điều này có thể được thực hiện trong vài dòng mã.

Để tạo container, TestContainers cung cấp một mẫu builder để cấu hình container. Ở đây, có thể định nghĩa hình ảnh (điều này có thể hữu ích nếu bạn có hình ảnh tùy chỉnh) và nhiều hơn nữa. Hiện tại, chúng ta sẽ chỉ sử dụng hình ảnh mặc định.

var postgresContainer = new PostgreSqlBuilder() // .WithImage("...") .Build();

Container này có thể được bắt đầu và bạn đã sẵn sàng! Như một ví dụ, hãy tạo kết nối đến cơ sở dữ liệu và thực thi một truy vấn đơn giản.

Tích hợp container kiểm thử vào các bài kiểm thử của bạn

Mã trên cho bạn ý tưởng về cách sử dụng TestContainers, nhưng nó không thực tế lắm. Chúng ta không muốn lặp lại việc thiết lập và dọn dẹp container trong mỗi bài kiểm thử.

Trong integration tests ASP.NET, chúng ta cũng muốn thay thế kết nối cơ sở dữ liệu của ứng dụng bằng kết nối đến container kiểm thử.

Để đạt được điều này, chúng ta có thể tạo một WebApplicationFactory tùy chỉnh để cấu hình ứng dụng cho kiểm thử.

Áp dụng EF migrations

Để áp dụng EF migrations, tôi cũng cấu hình database context trong phương thức ConfigureServices để bao gồm migrations assembly. Sau khi container được bắt đầu, các migrations được áp dụng.

Ví dụ về trường hợp kiểm thử

Bây giờ chúng ta có CustomerApiWebApplicationFactory, chúng ta có thể viết integration tests chạy chống lại một instance PostgreSQL cô lập với các migrations mới nhất được áp dụng. Để tăng tốc các bài kiểm thử, chúng ta có thể chia sẻ cùng một instance của WebApplicationFactory trên nhiều bài kiểm thử.

Bài kiểm thử tạo một HTTP client mới cho API của chúng ta, tạo một khách hàng mới sử dụng endpoint POST và truy xuất khách hàng sử dụng endpoint GET. Cuối cùng, nó khẳng định rằng phản hồi như mong đợi.

Bạn có thể nhận thấy rằng bài kiểm thử có nhiều khẳng định hơn (một để tạo khách hàng, một để truy xuất nó) so với một unit test, nhưng tôi thích điều này vì nó bao phủ luồng chỉ bằng các cổng bên ngoài của ứng dụng. Điều này ngăn chúng ta kiểm tra các chi tiết triển khai nội bộ, điều mà tôi rất muốn tránh.

Còn về pipeline CI thì sao?

Bộ kiểm thử của bạn trở nên tự chứa khi bạn thay thế tất cả các hệ thống bên ngoài bằng các container kiểm thử. Điều này làm cho việc chạy các bài kiểm thử này ở bất cứ đâu trở nên dễ dàng, bao gồm cả pipeline CI của bạn.

Giống như với môi trường phát triển cục bộ, yêu cầu duy nhất là máy chạy các bài kiểm thử đã cài đặt và đang chạy Docker. May mắn thay, hầu hết các agent CI được lưu trữ (bao gồm Azure DevOps và GitHub) đã có Docker cài đặt, vì vậy bạn không phải lo lắng về điều đó.

Kết luận

Cá nhân tôi tin rằng integration tests mang lại nhiều giá trị và sự tin tưởng rằng hệ thống hoạt động như mong đợi. Đó là lý do tại sao tôi thích viết integration tests hơn unit tests cho các ứng dụng/endpoint không có nhiều logic.

Điều cần ghi nhớ khi viết integration tests là điều quan trọng để đảm bảo rằng chúng không tiêu thụ các hệ thống bên ngoài. Nếu không, có thể vô tình ảnh hưởng đến dữ liệu và người dùng thực. Trong quá khứ, điều này không phải lúc nào cũng dễ dàng, nhưng với TestContainers điều này trở nên dễ dàng hơn nhiều.

Một container kiểm thử là một container Docker có thể được bắt đầu và dừng một cách lập trình, làm cho việc thiết lập và dọn dẹp cơ sở hạ tầng cần thiết cho các bài kiểm thử của bạn trở nên nhanh chóng mà không cần vượt qua các rào cản. TestContainers cũng cung cấp một môi trường cô lập cho mỗi bài kiểm thử, đảm bảo rằng các bài kiểm thử không can thiệp lẫn nhau. Vì điều này, bộ kiểm thử của bạn cũng có thể được chạy song song, điều này có thể mang lại tốc độ tăng đáng kể.

Tóm lại, một container kiểm thử làm cho việc viết các integration tests đáng tin cậy và lặp lại trở nên dễ dàng, mang lại cho bạn sự tin tưởng rằng ứng dụng của bạn hoạt động như mong đợi.

Đối với mã nguồn đầy đủ của các ví dụ trong bài đăng blog này, bạn có thể xem mã trên GitHub.

Chỉ mục