.NET集成测试 dotnet-integration-testing

.NET集成测试技能专注于使用WebApplicationFactory、Testcontainers和.NET Aspire进行自动化测试,涵盖API测试、数据库管理、测试隔离等,适用于.NET应用程序的集成测试和基础设施验证。关键词:.NET, 集成测试, WebApplicationFactory, Testcontainers, .NET Aspire, 数据库测试, 测试隔离, 自动化测试, 基础设施测试。

测试 0 次安装 0 次浏览 更新于 3/6/2026

名称: .NET集成测试 描述: “使用真实基础设施进行测试。WebApplicationFactory、Testcontainers、Aspire、fixtures。” 用户可调用: false

.NET集成测试

.NET应用程序的集成测试模式,使用WebApplicationFactory、Testcontainers和.NET Aspire进行测试。涵盖进程内API测试、通过容器的可处置基础设施、数据库fixture管理和测试隔离策略。

版本假设: .NET 8.0+基线,Testcontainers 3.x+,.NET Aspire 9.0+。Microsoft.AspNetCore.Mvc.Testing的包版本必须匹配项目的目标框架主版本(例如,net8.0对应8.x,net9.0对应9.x,net10.0对应10.x)。以下示例使用Testcontainers 4.x API;模式同样适用于3.x,仅命名空间有细微差异。

范围

  • 使用WebApplicationFactory进行进程内API测试
  • 通过Testcontainers实现可处置基础设施
  • .NET Aspire分布式应用程序测试
  • 数据库fixture管理和测试隔离
  • 认证和授权测试设置

超出范围

  • 测试项目脚手架(创建项目、包引用)——参见[技能:dotnet-add-testing]
  • 测试策略和测试类型选择——参见[技能:dotnet-testing-strategy]
  • 用于验证API响应结构的快照测试——参见[技能:dotnet-snapshot-testing]

先决条件: 测试项目已通过[技能:dotnet-add-testing]搭建,并引用了集成测试包。Docker守护进程运行(Testcontainers要求)。运行[技能:dotnet-version-detection]确认.NET 8.0+基线。

交叉参考: [技能:dotnet-testing-strategy]用于决定何时使用集成测试,[技能:dotnet-xunit]用于xUnit fixtures和并行执行配置,[技能:dotnet-snapshot-testing]用于使用Verify验证API响应结构。


WebApplicationFactory

WebApplicationFactory<TEntryPoint>为ASP.NET Core应用程序创建进程内测试服务器。测试发送HTTP请求而无需网络开销,执行完整的中间件管道、路由、模型绑定和序列化。

<!-- 版本必须匹配目标框架:net8.0对应8.x,net9.0对应9.x等 -->
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />

基本用法

public class OrdersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public OrdersApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task GetOrders_ReturnsOkWithJsonArray()
    {
        var response = await _client.GetAsync("/api/orders");

        response.EnsureSuccessStatusCode();
        var orders = await response.Content
            .ReadFromJsonAsync<List<OrderDto>>();
        Assert.NotNull(orders);
    }

    [Fact]
    public async Task CreateOrder_ValidPayload_Returns201()
    {
        var request = new CreateOrderRequest
        {
            CustomerId = "cust-123",
            Items = [new("SKU-001", Quantity: 2)]
        };

        var response = await _client.PostAsJsonAsync("/api/orders", request);

        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
        Assert.NotNull(response.Headers.Location);
    }
}

重要: Program类必须对测试项目可访问。要么使其公开,要么添加InternalsVisibleTo属性:

// 在API项目中(例如,Program.cs或单独文件)
[assembly: InternalsVisibleTo("MyApp.Api.IntegrationTests")]

或在csproj中:

<ItemGroup>
  <InternalsVisibleTo Include="MyApp.Api.IntegrationTests" />
</ItemGroup>

自定义测试服务器

使用WebApplicationFactory<T>.WithWebHostBuilder重写服务、配置或中间件:

public class CustomWebAppFactory : WebApplicationFactory<Program>
{
    // 从测试fixture(例如,Testcontainers)提供连接字符串
    public string ConnectionString { get; set; } = "";

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseEnvironment("Testing");

        builder.ConfigureAppConfiguration((context, config) =>
        {
            config.AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["ConnectionStrings:Default"] = ConnectionString,
                ["Features:EnableNewCheckout"] = "true"
            });
        });

        builder.ConfigureTestServices(services =>
        {
            // 用测试替代品替换真实服务
            services.RemoveAll<IEmailSender>();
            services.AddSingleton<IEmailSender, FakeEmailSender>();

            // 用测试数据库替换数据库上下文
            services.RemoveAll<DbContextOptions<AppDbContext>>();
            services.AddDbContext<AppDbContext>(options =>
                options.UseNpgsql(ConnectionString));
        });
    }
}

认证请求

通过配置认证处理程序测试认证端点:

public class AuthenticatedWebAppFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            services.AddAuthentication("Test")
                .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                    "Test", options => { });
        });
    }
}

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.NameIdentifier, "test-user-id"),
            new Claim(ClaimTypes.Name, "Test User"),
            new Claim(ClaimTypes.Role, "Admin")
        };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "Test");

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

Testcontainers

Testcontainers在Docker容器中为测试启动真实基础设施(数据库、消息代理、缓存)。每个测试运行获得一个全新、可处置的环境。

<PackageReference Include="Testcontainers" Version="4.*" />
<!-- 数据库特定模块 -->
<PackageReference Include="Testcontainers.PostgreSql" Version="4.*" />
<PackageReference Include="Testcontainers.MsSql" Version="4.*" />
<PackageReference Include="Testcontainers.Redis" Version="4.*" />

PostgreSQL示例

public class PostgresFixture : IAsyncLifetime
{
    private readonly PostgreSqlContainer _container = new PostgreSqlBuilder()
        .WithImage("postgres:16-alpine")
        .WithDatabase("testdb")
        .WithUsername("test")
        .WithPassword("test")
        .Build();

    public string ConnectionString => _container.GetConnectionString();

    public async ValueTask InitializeAsync()
    {
        await _container.StartAsync();
    }

    public async ValueTask DisposeAsync()
    {
        await _container.DisposeAsync();
    }
}

[CollectionDefinition("Postgres")]
public class PostgresCollection : ICollectionFixture<PostgresFixture> { }

[Collection("Postgres")]
public class OrderRepositoryTests
{
    private readonly PostgresFixture _postgres;

    public OrderRepositoryTests(PostgresFixture postgres)
    {
        _postgres = postgres;
    }

    [Fact]
    public async Task Insert_ValidOrder_CanBeRetrieved()
    {
        await using var context = CreateContext(_postgres.ConnectionString);
        await context.Database.EnsureCreatedAsync();

        var order = new Order { CustomerId = "cust-1", Total = 99.99m };
        context.Orders.Add(order);
        await context.SaveChangesAsync();

        var retrieved = await context.Orders.FindAsync(order.Id);
        Assert.NotNull(retrieved);
        Assert.Equal(99.99m, retrieved.Total);
    }

    private static AppDbContext CreateContext(string connectionString)
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseNpgsql(connectionString)
            .Options;
        return new AppDbContext(options);
    }
}

SQL Server示例

public class SqlServerFixture : IAsyncLifetime
{
    private readonly MsSqlContainer _container = new MsSqlBuilder()
        .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
        .Build();

    public string ConnectionString => _container.GetConnectionString();

    public async ValueTask InitializeAsync()
    {
        await _container.StartAsync();
    }

    public async ValueTask DisposeAsync()
    {
        await _container.DisposeAsync();
    }
}

结合WebApplicationFactory与Testcontainers

最常见的模式:使用Testcontainers处理数据库,WebApplicationFactory处理API:

public class ApiTestFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
    private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
        .WithImage("postgres:16-alpine")
        .Build();

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            services.RemoveAll<DbContextOptions<AppDbContext>>();
            services.AddDbContext<AppDbContext>(options =>
                options.UseNpgsql(_postgres.GetConnectionString()));
        });
    }

    public async ValueTask InitializeAsync()
    {
        await _postgres.StartAsync();
    }

    public new async ValueTask DisposeAsync()
    {
        await _postgres.DisposeAsync();
        await base.DisposeAsync();
    }
}

public class OrdersApiIntegrationTests : IClassFixture<ApiTestFactory>
{
    private readonly HttpClient _client;
    private readonly ApiTestFactory _factory;

    public OrdersApiIntegrationTests(ApiTestFactory factory)
    {
        _factory = factory;
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task CreateAndRetrieveOrder_RoundTrip()
    {
        // 确保架构存在
        using var scope = _factory.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        await db.Database.EnsureCreatedAsync();

        // 创建
        var createResponse = await _client.PostAsJsonAsync("/api/orders",
            new { CustomerId = "cust-1", Items = new[] { new { Sku = "SKU-1", Quantity = 2 } } });
        createResponse.EnsureSuccessStatusCode();
        var location = createResponse.Headers.Location!.ToString();

        // 检索
        var getResponse = await _client.GetAsync(location);
        getResponse.EnsureSuccessStatusCode();
        var order = await getResponse.Content.ReadFromJsonAsync<OrderDto>();

        Assert.Equal("cust-1", order!.CustomerId);
    }
}

.NET Aspire测试

.NET Aspire提供DistributedApplicationTestingBuilder用于测试使用Aspire编排的多服务应用程序。这测试实际的分布式拓扑,包括服务发现、配置和健康检查。

<PackageReference Include="Aspire.Hosting.Testing" Version="9.*" />

基本Aspire测试

public class AspireIntegrationTests
{
    [Fact]
    public async Task ApiService_ReturnsHealthy()
    {
        var builder = await DistributedApplicationTestingBuilder
            .CreateAsync<Projects.MyApp_AppHost>();

        await using var app = await builder.BuildAsync();
        await app.StartAsync();

        var httpClient = app.CreateHttpClient("api-service");

        var response = await httpClient.GetAsync("/health");

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

    [Fact]
    public async Task ApiService_WithDatabase_ReturnsOrders()
    {
        var builder = await DistributedApplicationTestingBuilder
            .CreateAsync<Projects.MyApp_AppHost>();

        await using var app = await builder.BuildAsync();
        await app.StartAsync();

        // 等待资源健康
        var resourceNotification = app.Services
            .GetRequiredService<ResourceNotificationService>();
        await resourceNotification
            .WaitForResourceHealthyAsync("api-service")
            .WaitAsync(TimeSpan.FromSeconds(60));

        var httpClient = app.CreateHttpClient("api-service");
        var response = await httpClient.GetAsync("/api/orders");

        response.EnsureSuccessStatusCode();
    }
}

Aspire与服务覆盖

替换Aspire应用模型中的服务以进行测试:

[Fact]
public async Task ApiService_WithMockedExternalDependency()
{
    var builder = await DistributedApplicationTestingBuilder
        .CreateAsync<Projects.MyApp_AppHost>();

    // 覆盖API服务的配置
    builder.Services.ConfigureHttpClientDefaults(http =>
    {
        http.AddStandardResilienceHandler();
    });

    await using var app = await builder.BuildAsync();
    await app.StartAsync();

    var httpClient = app.CreateHttpClient("api-service");
    var response = await httpClient.GetAsync("/api/orders");

    response.EnsureSuccessStatusCode();
}

数据库Fixture模式

使用事务的每测试隔离

使用事务范围回滚每个测试的更改:

public class TransactionalTestBase : IClassFixture<PostgresFixture>, IAsyncLifetime
{
    private readonly PostgresFixture _postgres;
    private AppDbContext _context = null!;
    private IDbContextTransaction _transaction = null!;

    public TransactionalTestBase(PostgresFixture postgres)
    {
        _postgres = postgres;
    }

    protected AppDbContext Context => _context;

    public async ValueTask InitializeAsync()
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseNpgsql(_postgres.ConnectionString)
            .Options;
        _context = new AppDbContext(options);
        await _context.Database.EnsureCreatedAsync();
        _transaction = await _context.Database.BeginTransactionAsync();
    }

    public async ValueTask DisposeAsync()
    {
        await _transaction.RollbackAsync();
        await _transaction.DisposeAsync();
        await _context.DisposeAsync();
    }
}

public class OrderTests : TransactionalTestBase
{
    public OrderTests(PostgresFixture postgres) : base(postgres) { }

    [Fact]
    public async Task Insert_ValidOrder_Persists()
    {
        Context.Orders.Add(new Order { CustomerId = "cust-1", Total = 50m });
        await Context.SaveChangesAsync();

        var count = await Context.Orders.CountAsync();
        Assert.Equal(1, count);
        // 测试后事务回滚——数据库保持清洁
    }
}

使用Respawn的每测试隔离

使用Respawn通过删除数据重置数据库状态,这在事务回滚不可行时有用(例如,测试代码提交自己的事务):

// NuGet: Respawn
// 组合fixture:拥有容器和respawner
public class RespawnablePostgresFixture : IAsyncLifetime
{
    private readonly PostgreSqlContainer _container = new PostgreSqlBuilder()
        .WithImage("postgres:16-alpine")
        .Build();

    private Respawner _respawner = null!;
    private NpgsqlConnection _connection = null!;

    public string ConnectionString => _container.GetConnectionString();

    public async ValueTask InitializeAsync()
    {
        await _container.StartAsync();

        _connection = new NpgsqlConnection(ConnectionString);
        await _connection.OpenAsync();

        // 在创建respawner之前运行迁移或EnsureCreated
        // 以便它知道要清理哪些表
        _respawner = await Respawner.CreateAsync(_connection, new RespawnerOptions
        {
            DbAdapter = DbAdapter.Postgres,
            TablesToIgnore = ["__EFMigrationsHistory"]
        });
    }

    public async Task ResetDatabaseAsync()
    {
        await _respawner.ResetAsync(_connection);
    }

    public async ValueTask DisposeAsync()
    {
        await _connection.DisposeAsync();
        await _container.DisposeAsync();
    }
}

测试隔离策略

策略比较

策略 速度 隔离性 复杂性 最佳适用
事务回滚 最快 使用单个DbContext的测试
Respawn(数据删除) 中等 代码提交自己事务的测试
每类新容器 最高 修改架构或需要完全隔离的测试
共享容器 + 清理 中等 中等 中等 多个类共享基础设施的测试套件

容器生命周期建议

每测试:      太慢。永远不要每个测试启动一个容器。
每类:       良好的隔离性,速度可接受,使用ICollectionFixture。
每集合:     最佳平衡——在相关测试类之间共享一个容器。
每程序集:   最快但需要测试间仔细清理。

使用ICollectionFixture<T>(参见[技能:dotnet-xunit])在多个测试类之间共享单个容器,同时顺序运行这些类以避免数据冲突。


使用Redis进行测试

public class RedisFixture : IAsyncLifetime
{
    private readonly RedisContainer _container = new RedisBuilder()
        .WithImage("redis:7-alpine")
        .Build();

    public string ConnectionString => _container.GetConnectionString();

    public async ValueTask InitializeAsync() => await _container.StartAsync();
    public async ValueTask DisposeAsync() => await _container.DisposeAsync();
}

[CollectionDefinition("Redis")]
public class RedisCollection : ICollectionFixture<RedisFixture> { }

[Collection("Redis")]
public class CacheServiceTests
{
    private readonly RedisFixture _redis;

    public CacheServiceTests(RedisFixture redis) => _redis = redis;

    [Fact]
    public async Task SetAndGet_RoundTrip_ReturnsOriginalValue()
    {
        var multiplexer = await ConnectionMultiplexer.ConnectAsync(
            _redis.ConnectionString);
        var cache = new RedisCacheService(multiplexer);

        await cache.SetAsync("key-1", new Order { Id = 1, Total = 99m });
        var result = await cache.GetAsync<Order>("key-1");

        Assert.NotNull(result);
        Assert.Equal(99m, result.Total);
    }
}

关键原则

  • 使用WebApplicationFactory进行API测试。 比测试部署实例更快、更可靠、更确定。
  • 使用Testcontainers处理真实基础设施。 不要模拟DbContext——针对真实数据库测试以验证LINQ-to-SQL翻译和约束执行。
  • 通过ICollectionFixture在测试类之间共享容器,避免每个类启动新容器的开销。
  • 选择正确的隔离策略。 事务回滚是最快最简单的;当无法控制事务边界时使用Respawn。
  • 始终清理测试数据。 一个测试的残留数据会导致另一个测试的脆弱失败。使用事务回滚、Respawn或新容器。
  • 匹配Microsoft.AspNetCore.Mvc.Testing版本到TFM。 使用错误版本会导致运行时绑定失败。

代理注意事项

  1. 不要硬编码Microsoft.AspNetCore.Mvc.Testing版本。 包版本必须匹配项目的目标框架主版本。指定例如Version="8.0.0"会破坏net9.0项目。
  2. 不要忘记InternalsVisibleTo用于Program类。 没有它,WebApplicationFactory<Program>无法访问入口点,测试在编译时失败。
  3. 不要将EnsureCreated()与Respawn一起使用。 EnsureCreated()不跟踪迁移。对于生产架构使用Database.MigrateAsync(),或仅用于简单测试架构的EnsureCreated()
  4. 不要在HttpClient之前处置WebApplicationFactory 工厂拥有测试服务器;处置它会使所有客户端无效。让xUnit通过IClassFixture管理处置。
  5. 不要在Testcontainers中使用localhost端口。 Testcontainers将随机主机端口映射到容器端口。始终使用容器对象的连接字符串(例如,_container.GetConnectionString()),永远不要硬编码端口。
  6. 不要在CI中跳过Docker可用性检查。 Testcontainers需要运行的Docker守护进程。确保CI环境有Docker可用,或在Docker不可用时使用条件测试跳过。

参考