Kiểm thử ứng dụng Minimal API
Nguồn: Test Minimal API apps
Bởi Fiyaz Bin Hasan và Rick Anderson
Giới thiệu về integration test (kiểm thử tích hợp)
Integration test (kiểm thử tích hợp) đánh giá các thành phần của ứng dụng ở mức độ rộng hơn so với unit test (kiểm thử đơn vị). Unit test được sử dụng để kiểm thử các thành phần phần mềm riêng lẻ, chẳng hạn như các phương thức của lớp riêng biệt. Integration test xác nhận rằng hai hoặc nhiều thành phần ứng dụng hoạt động cùng nhau để tạo ra kết quả mong đợi, có thể bao gồm mọi thành phần cần thiết để xử lý đầy đủ một request.
Các test rộng hơn này được sử dụng để kiểm thử cơ sở hạ tầng và toàn bộ framework của ứng dụng, thường bao gồm các thành phần sau:
- Cơ sở dữ liệu
- Hệ thống tệp
- Thiết bị mạng
- Pipeline request-response (đường ống yêu cầu-phản hồi)
Unit test sử dụng các thành phần giả tạo, được gọi là *fake* hoặc *mock object* (đối tượng giả), thay thế cho các thành phần cơ sở hạ tầng.
Trái ngược với unit test, integration test:
- Sử dụng các thành phần thực tế mà ứng dụng sử dụng trong production.
- Yêu cầu nhiều mã và xử lý dữ liệu hơn.
- Mất nhiều thời gian hơn để chạy.
Do đó, hãy giới hạn việc sử dụng integration test cho các tình huống cơ sở hạ tầng quan trọng nhất. Nếu một hành vi có thể được kiểm thử bằng unit test hoặc integration test, hãy chọn unit test.
Trong các cuộc thảo luận về integration test, dự án được kiểm thử thường được gọi là System Under Test (Hệ thống đang được kiểm thử), hay "SUT". "SUT" được sử dụng xuyên suốt bài viết này để chỉ ứng dụng ASP.NET Core đang được kiểm thử.
Không nên viết integration test cho mọi hoán vị của dữ liệu và quyền truy cập tệp với cơ sở dữ liệu và hệ thống tệp. Bất kể có bao nhiêu nơi trong ứng dụng tương tác với cơ sở dữ liệu và hệ thống tệp, một bộ integration test đọc, ghi, cập nhật và xóa tập trung thường đủ để kiểm thử các thành phần cơ sở dữ liệu và hệ thống tệp. Sử dụng unit test cho các test thường xuyên về logic phương thức tương tác với các thành phần này.
ASP.NET Core integration test
Integration test trong ASP.NET Core yêu cầu:
- Một test project được sử dụng để chứa và thực thi các test. Test project có tham chiếu đến SUT.
- Test project tạo một test web host cho SUT và sử dụng client test server để xử lý các request và response với SUT.
- Một test runner (trình chạy test) được sử dụng để thực thi các test và báo cáo kết quả.
Integration test tuân theo một trình tự sự kiện bao gồm các bước test thông thường Arrange (Chuẩn bị), Act (Hành động) và Assert (Xác nhận):
- Web host của SUT được cấu hình.
- Client test server được tạo để gửi request đến ứng dụng.
- Bước test Arrange được thực thi: test app chuẩn bị request.
- Bước test Act được thực thi: client gửi request và nhận response.
- Bước test Assert được thực thi: response thực tế được xác thực là pass hoặc fail dựa trên response mong đợi.
- Quá trình tiếp tục cho đến khi tất cả các test được thực thi.
- Kết quả test được báo cáo.
Thông thường, test web host được cấu hình khác với web host bình thường của ứng dụng trong các lần chạy test. Ví dụ, cơ sở dữ liệu khác hoặc cài đặt ứng dụng khác có thể được sử dụng cho các test.
Các thành phần cơ sở hạ tầng, chẳng hạn như test web host và in-memory test server (TestServer), được cung cấp hoặc quản lý bởi gói Microsoft.AspNetCore.Mvc.Testing. Việc sử dụng gói này giúp đơn giản hóa việc tạo và thực thi test.
Gói Microsoft.AspNetCore.Mvc.Testing xử lý các nhiệm vụ sau:
- Sao chép file dependencies (
.deps) từ SUT vào thư mụcbincủa test project. - Đặt content root (thư mục gốc nội dung) thành thư mục gốc dự án của SUT để các tệp tĩnh và trang/view được tìm thấy khi các test được thực thi.
- Cung cấp lớp WebApplicationFactory để đơn giản hóa việc khởi động SUT với
TestServer.
Hãy tách unit test và integration test thành các dự án khác nhau. Việc tách biệt:
- Giúp đảm bảo các thành phần kiểm thử cơ sở hạ tầng không vô tình được đưa vào unit test.
- Cho phép kiểm soát tập hợp test nào được chạy.
Mã mẫu trên GitHub cung cấp ví dụ về unit test và integration test trên ứng dụng Minimal API.
Unit test các kiểu triển khai IResult
Ví dụ sau đây cho thấy cách unit test các minimal route handler (trình xử lý route tối giản) trả về IResult bằng framework kiểm thử xUnit. Cơ sở dữ liệu bên ngoài được thay thế bằng cơ sở dữ liệu trong bộ nhớ (in-memory database) trong quá trình kiểm thử.
Các kiểu triển khai IResult public trong namespace Microsoft.AspNetCore.Http.HttpResults có thể được sử dụng để unit test các minimal route handler khi sử dụng các named method (phương thức được đặt tên) thay vì lambda.
Đoạn code sau sử dụng lớp NotFound<TValue>:
[Fact]
public async Task GetTodoReturnsNotFoundIfNotExists()
{
// Arrange
await using var context = new MockDb().CreateDbContext();
// Act
var result = await TodoEndpointsV1.GetTodo(1, context);
//Assert
Assert.IsType<Results<Ok<Todo>, NotFound>>(result);
var notFoundResult = (NotFound) result.Result;
Assert.NotNull(notFoundResult);
}Đoạn code sau sử dụng lớp Ok<TValue>:
[Fact]
public async Task GetTodoReturnsTodoFromDatabase()
{
// Arrange
await using var context = new MockDb().CreateDbContext();
context.Todos.Add(new Todo
{
Id = 1,
Title = "Test title",
Description = "Test description",
IsDone = false
});
await context.SaveChangesAsync();
// Act
var result = await TodoEndpointsV1.GetTodo(1, context);
//Assert
Assert.IsType<Results<Ok<Todo>, NotFound>>(result);
var okResult = (Ok<Todo>)result.Result;
Assert.NotNull(okResult.Value);
Assert.Equal(1, okResult.Value.Id);
}Trong các ví dụ trước, kết quả được cast (ép kiểu) sang kiểu cụ thể vì endpoint đang được kiểm thử có thể trả về nhiều kiểu (kết quả NotFound<TValue> hoặc Ok<TValue>). Tuy nhiên, nếu endpoint trả về một kiểu TypedResults duy nhất, thì kết quả được tự động suy ra kiểu đó và không cần cast.
Đoạn code sau sử dụng lớp Ok và kiểu của giá trị là collection của Todo:
[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
// Arrange
await using var context = new MockDb().CreateDbContext();
context.Todos.Add(new Todo
{
Id = 1,
Title = "Test title 1",
Description = "Test description 1",
IsDone = false
});
context.Todos.Add(new Todo
{
Id = 2,
Title = "Test title 2",
Description = "Test description 2",
IsDone = true
});
await context.SaveChangesAsync();
// Act
var result = await TodoEndpointsV1.GetAllTodos(context);
//Assert
Assert.IsType<Ok<Todo[]>>(result);
Assert.NotNull(result.Value);
Assert.NotEmpty(result.Value);
Assert.Collection(result.Value, todo1 =>
{
Assert.Equal(1, todo1.Id);
Assert.Equal("Test title 1", todo1.Title);
Assert.False(todo1.IsDone);
}, todo2 =>
{
Assert.Equal(2, todo2.Id);
Assert.Equal("Test title 2", todo2.Title);
Assert.True(todo2.IsDone);
});
}