Nguon: Microsoft Learn · .NET 8.0

Kiểm thử ứng dụng Minimal API

Nguồn: Test Minimal API apps

Bởi Fiyaz Bin HasanRick 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:

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:

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:

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):

  1. Web host của SUT được cấu hình.
  2. Client test server được tạo để gửi request đến ứng dụng.
  3. Bước test Arrange được thực thi: test app chuẩn bị request.
  4. Bước test Act được thực thi: client gửi request và nhận response.
  5. 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.
  6. Quá trình tiếp tục cho đến khi tất cả các test được thực thi.
  7. 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:

Hãy tách unit test và integration test thành các dự án khác nhau. Việc tách biệt:

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>:

csharp
[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&lt;TValue&gt;:

csharp
[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&lt;TValue&gt; hoặc Ok&lt;TValue&gt;). 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:

csharp
[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);
    });
}