Nguon: Microsoft Learn · .NET 8.0

Kiểm thử (Test) ứng dụng Minimal API

Nguồn: Test Minimal API apps

Giới thiệu về integration tests (kiểm thử tích hợp)

Integration tests (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 tests (kiểm thử đơn vị). Unit tests được dùng để kiểm thử các thành phần phần mềm riêng lẻ, như các phương thức lớp đơn lẻ. Integration tests 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 bài kiểm thử rộng hơn này được dùng để kiểm thử cơ sở hạ tầng của ứng dụng và toàn bộ framework, thường bao gồm các thành phần sau:

Unit tests sử dụng các thành phần giả mạo, được gọi là fakes hoặc mock objects, thay thế cho các thành phần cơ sở hạ tầng.

Trái ngược với unit tests, integration tests:

Do đó, hãy giới hạn việc sử dụng integration tests 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 thảo luận về integration tests, project được kiểm thử thường được gọi là System Under Test, hay "SUT". "SUT" được 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 viết integration tests cho mọi hoán vị dữ liệu và truy cập file với database và file system. Bất kể có bao nhiêu nơi trong ứng dụng tương tác với database và file system, một tập hợp tập trung các integration tests đọc, viết, cập nhật và xóa thường đủ để kiểm thử đúng các thành phần database và file system. Dùng unit tests cho các bài kiểm thử thường xuyên về logic phương thức tương tác với các thành phần này.

Integration tests trong ASP.NET Core

Integration tests trong ASP.NET Core yêu cầu:

Integration tests tuân theo chuỗi sự kiện bao gồm các bước test Arrange (chuẩn bị), Act (thực hiện), và Assert (kiểm tra) thông thường:

  1. SUT web host được cấu hình.
  2. Test server client được tạo để gửi request đến ứng dụng.
  3. Bước test Arrange: Test app chuẩn bị request.
  4. Bước test Act: Client gửi request và nhận response.
  5. Bước test Assert: Response thực tế được xác nhận là pass (vượt qua) hoặc fail (thất bại) dựa trên response mong đợi.
  6. Quá trình tiếp tục cho đến khi tất cả test được thực thi.
  7. Kết quả test được báo cáo.

Thường thì test web host được cấu hình khác với web host thông thường của ứng dụng cho các lần chạy test. Ví dụ, có thể dùng database khác hoặc cài đặt ứng dụng khác cho test.

Các thành phần cơ sở hạ tầng như test web host và in-memory test server (TestServer) được cung cấp hoặc quản lý bởi package Microsoft.AspNetCore.Mvc.Testing. Việc sử dụng package này giúp đơn giản hóa việc tạo và thực thi test.

Package Microsoft.AspNetCore.Mvc.Testing xử lý các tác vụ sau:

Tách biệt unit tests và integration tests thành các project khác nhau. Việc tách biệt:

Unit test kiểu triển khai IResult

Ví dụ sau cho thấy cách unit test các route handler minimal trả về IResult bằng framework kiểm thử xUnit. Database ngoại vi được thay thế bằng in-memory database trong quá trình kiểm thử.

Các kiểu triển khai IResult công khai trong namespace Microsoft.AspNetCore.Http.HttpResults có thể được dùng để unit test các route handler minimal khi dùng phương thức được đặt tên thay vì lambda.

Code sau dùng lớp NotFound<TValue>:

csharp
[Fact]
public async Task GetTodoReturnsNotFoundIfNotExists()
{
    // Arrange (Chuẩn bị)
    await using var context = new MockDb().CreateDbContext();

    // Act (Thực hiện)
    var result = await TodoEndpointsV1.GetTodo(1, context);

    //Assert (Kiểm tra)
    Assert.IsType<Results<Ok<Todo>, NotFound>>(result);

    var notFoundResult = (NotFound) result.Result;

    Assert.NotNull(notFoundResult);
}

Code sau dùng lớp Ok<TValue>:

csharp
[Fact]
public async Task GetTodoReturnsTodoFromDatabase()
{
    // Arrange (Chuẩn bị)
    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 (Thực hiện)
    var result = await TodoEndpointsV1.GetTodo(1, context);

    //Assert (Kiểm tra)
    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 ép kiểu sang kiểu cụ thể vì endpoint đang được kiểm thử có thể trả về nhiều kiểu. Tuy nhiên, nếu endpoint trả về kiểu TypedResults đơn, thì kết quả được suy ra tự động về kiểu đó và không cần ép kiểu.

Code sau dùng lớp Ok và kiểu value là collection Todo:

csharp
[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
    // Arrange (Chuẩn bị)
    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 (Thực hiện)
    var result = await TodoEndpointsV1.GetAllTodos(context);

    //Assert (Kiểm tra)
    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);
    });
}