Nguon: Microsoft Learn · .NET 8.0

Kiểm thử tích hợp (Integration Tests) trong ASP.NET Core

Nguồn: Integration tests in ASP.NET Core

Tổng quan

Integration tests (kiểm thử tích hợp) đảm bảo các thành phần của ứng dụng hoạt động đúng ở mức độ bao gồm cả cơ sở hạ tầng hỗ trợ, như cơ sở dữ liệu, hệ thống tệp và mạng. ASP.NET Core hỗ trợ integration tests bằng cách sử dụng một unit test framework với một test web host (máy chủ web kiểm thử) và một in-memory test server (máy chủ kiểm thử trong bộ nhớ).

Giới thiệu về Integration Tests

Integration tests đá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ị):

Đặc điểm chính

Integration tests:

Các thực hành tốt nhất

Yêu cầu Integration Test trong ASP.NET Core

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

  1. Một dự án kiểm thử chứa và thực thi các test (có tham chiếu đến System Under Test - SUT)
  2. Một test web host cho SUT và một test server client để xử lý requests/responses
  3. Một test runner để thực thi tests và báo cáo kết quả

Thứ tự kiểm thử

  1. Web host của SUT được cấu hình
  2. Tạo một test server client để gửi requests
  3. Arrange (Chuẩn bị): Ứng dụng kiểm thử chuẩn bị một request
  4. Act (Thực hiện): Client gửi request và nhận response
  5. Assert (Kiểm tra): Response được xác nhận so với kết quả mong đợi
  6. Quá trình tiếp tục cho đến khi tất cả các tests đã thực thi
  7. Kết quả test được báo cáo

Điều kiện tiên quyết cho dự án kiểm thử

Dự án kiểm thử phải:

Các phụ thuộc bổ sung

Đối với ứng dụng mẫu sử dụng xUnit và AngleSharp:

xml
<PackageReference Include="AngleSharp" Version="X.X.X" />
<PackageReference Include="xunit" Version="X.X.X" />
<PackageReference Include="xunit.runner.visualstudio" Version="X.X.X" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="X.X.X" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="X.X.X" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="X.X.X" />

Kiểm thử cơ bản với WebApplicationFactory mặc định

Ví dụ đơn giản (xUnit)

csharp
public class BasicTests 
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public BasicTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}

Phương thức CreateClient() tạo một instance HttpClient tự động theo dõi redirects và xử lý cookies.

Tùy chỉnh WebApplicationFactory

Triển khai Custom Factory

csharp
public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType == 
                    typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType == typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Tạo SqliteConnection mở để EF không tự động đóng nó
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();
                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}

Sử dụng Custom Factory trong Tests

csharp
public class IndexPageTests :
    IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<Program> _factory;

    public IndexPageTests(CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }

    [Fact]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));

        // Assert
        Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
        Assert.Equal("/", response.Headers.Location.OriginalString);
    }
}

Tùy chỉnh Client với WithWebHostBuilder

csharp
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();
        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}

Tùy chọn Client

Tạo WebApplicationFactoryClientOptions để tùy chỉnh hành vi client:

csharp
var client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
    AllowAutoRedirect = false,
    BaseAddress = new Uri("https://localhost"), // Dùng cho HTTPS redirection
    HandleCookies = true,
    MaxAutomaticRedirections = 7
});

Lưu ý: Để tránh cảnh báo HTTPS redirection, đặt BaseAddress = new Uri("https://localhost")

Chèn Mock Services (dịch vụ giả lập)

Triển khai Service

csharp
public interface IQuoteService
{
    Task<string> GenerateQuote();
}

public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}

// Trong Program.cs:
services.AddScoped<IQuoteService, QuoteService>();

Test Mock Service

csharp
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}

[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}

Mock Authentication (Xác thực giả lập)

Authentication Handler

csharp
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        return Task.FromResult(AuthenticateResult.Success(ticket));
    }
}

Kiểm thử truy cập chưa xác thực

csharp
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login",
        response.Headers.Location.OriginalString);
}

Kiểm thử truy cập đã xác thực

csharp
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication(options =>
                    {
                        options.DefaultAuthenticateScheme = "TestScheme";
                        options.DefaultChallengeScheme = "TestScheme";
                    })
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "TestScheme", options => { });
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

Xử lý Antiforgery (chống giả mạo yêu cầu)

Ứng dụng mẫu sử dụng AngleSharp để phân tích HTML và xử lý antiforgery token. Các phương thức helper bao gồm:

csharp
var response = await _client.SendAsync(
    (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
    (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));

Ghi chú bổ sung