aspire-integration-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Integration Testing with .NET Aspire + xUnit

使用.NET Aspire + xUnit进行集成测试

When to Use This Skill

适用场景

Use this skill when:
  • Writing integration tests for .NET Aspire applications
  • Testing ASP.NET Core apps with real database connections
  • Verifying service-to-service communication in distributed applications
  • Testing with actual infrastructure (SQL Server, Redis, message queues) in containers
  • Combining Playwright UI tests with Aspire-orchestrated services
  • Testing microservices with proper service discovery and networking
在以下场景中使用本技能:
  • 为.NET Aspire应用编写集成测试
  • 测试带有真实数据库连接的ASP.NET Core应用
  • 验证分布式应用中的服务间通信
  • 在容器中使用实际基础设施(SQL Server、Redis、消息队列)进行测试
  • 将Playwright UI测试与Aspire编排的服务相结合
  • 通过正确的服务发现和网络测试微服务

Reference Files

参考文件

  • advanced-patterns.md: Endpoint discovery, database testing, Playwright, conditional config, Respawn, service communication, message queues
  • ci-and-tooling.md: CI/CD integration, custom resource waiters, Aspire CLI with MCP
  • advanced-patterns.md:端点发现、数据库测试、Playwright、条件配置、Respawn、服务通信、消息队列
  • ci-and-tooling.md:CI/CD集成、自定义资源等待器、结合MCP的Aspire CLI

Core Principles

核心原则

  1. Real Dependencies - Use actual infrastructure (databases, caches) via Aspire, not mocks
  2. Dynamic Port Binding - Let Aspire assign ports dynamically (
    127.0.0.1:0
    ) to avoid conflicts
  3. Fixture Lifecycle - Use
    IAsyncLifetime
    for proper test fixture setup and teardown
  4. Endpoint Discovery - Never hard-code URLs; discover endpoints from Aspire at runtime
  5. Parallel Isolation - Use xUnit collections to control test parallelization
  6. Health Checks - Always wait for services to be healthy before running tests
  1. 真实依赖项 - 通过Aspire使用实际基础设施(数据库、缓存),而非模拟对象
  2. 动态端口绑定 - 让Aspire动态分配端口(
    127.0.0.1:0
    )以避免冲突
  3. 夹具生命周期 - 使用
    IAsyncLifetime
    实现正确的测试夹具设置和清理
  4. 端点发现 - 绝不硬编码URL;在运行时从Aspire发现端点
  5. 并行隔离 - 使用xUnit集合控制测试并行化
  6. 健康检查 - 在运行测试前始终等待服务进入健康状态

High-Level Testing Architecture

高级测试架构

┌─────────────────┐                    ┌──────────────────────┐
│ xUnit test file │──uses────────────►│  AspireFixture       │
└─────────────────┘                    │  (IAsyncLifetime)    │
                                       └──────────────────────┘
                                               │ starts
                                    ┌───────────────────────────┐
                                    │  DistributedApplication   │
                                    │  (from AppHost)           │
                                    └───────────────────────────┘
                                               │ exposes
                                  ┌──────────────────────────────┐
                                  │   Dynamic HTTP Endpoints     │
                                  └──────────────────────────────┘
                                               │ consumed by
                                   ┌─────────────────────────┐
                                   │  HttpClient / Playwright│
                                   └─────────────────────────┘
┌─────────────────┐                    ┌──────────────────────┐
│ xUnit test file │──uses────────────►│  AspireFixture       │
└─────────────────┘                    │  (IAsyncLifetime)    │
                                       └──────────────────────┘
                                               │ starts
                                    ┌───────────────────────────┐
                                    │  DistributedApplication   │
                                    │  (from AppHost)           │
                                    └───────────────────────────┘
                                               │ exposes
                                  ┌──────────────────────────────┐
                                  │   Dynamic HTTP Endpoints     │
                                  └──────────────────────────────┘
                                               │ consumed by
                                   ┌─────────────────────────┐
                                   │  HttpClient / Playwright│
                                   └─────────────────────────┘

Required NuGet Packages

所需NuGet包

xml
<ItemGroup>
  <PackageReference Include="Aspire.Hosting.Testing" Version="$(AspireVersion)" />
  <PackageReference Include="xunit" Version="*" />
  <PackageReference Include="xunit.runner.visualstudio" Version="*" />
  <PackageReference Include="Microsoft.NET.Test.Sdk" Version="*" />
</ItemGroup>
xml
<ItemGroup>
  <PackageReference Include="Aspire.Hosting.Testing" Version="$(AspireVersion)" />
  <PackageReference Include="xunit" Version="*" />
  <PackageReference Include="xunit.runner.visualstudio" Version="*" />
  <PackageReference Include="Microsoft.NET.Test.Sdk" Version="*" />
</ItemGroup>

CRITICAL: File Watcher Fix for Integration Tests

关键:集成测试的文件监视器修复

When running many integration tests that each start an IHost, the default .NET host builder enables file watchers for configuration reload. This exhausts file descriptor limits on Linux.
Add this to your test project before any tests run:
csharp
// TestEnvironmentInitializer.cs
using System.Runtime.CompilerServices;

namespace YourApp.Tests;

internal static class TestEnvironmentInitializer
{
    [ModuleInitializer]
    internal static void Initialize()
    {
        // Disable config file watching in test hosts
        // Prevents file descriptor exhaustion (inotify watch limit) on Linux
        Environment.SetEnvironmentVariable("DOTNET_HOSTBUILDER__RELOADCONFIGONCHANGE", "false");
    }
}
当运行多个各自启动IHost的集成测试时,默认的.NET主机生成器会启用配置重载的文件监视器。这会在Linux上耗尽文件描述符限制。
在运行任何测试前,将以下代码添加到测试项目中:
csharp
// TestEnvironmentInitializer.cs
using System.Runtime.CompilerServices;

namespace YourApp.Tests;

internal static class TestEnvironmentInitializer
{
    [ModuleInitializer]
    internal static void Initialize()
    {
        // 禁用测试主机中的配置文件监视
        // 防止Linux上的文件描述符耗尽(inotify监视限制)
        Environment.SetEnvironmentVariable("DOTNET_HOSTBUILDER__RELOADCONFIGONCHANGE", "false");
    }
}

Pattern 1: Basic Aspire Test Fixture (Modern API)

模式1:基础Aspire测试夹具(现代API)

csharp
using Aspire.Hosting;
using Aspire.Hosting.Testing;

public sealed class AspireAppFixture : IAsyncLifetime
{
    private DistributedApplication? _app;

    public DistributedApplication App => _app
        ?? throw new InvalidOperationException("App not initialized");

    public async Task InitializeAsync()
    {
        var builder = await DistributedApplicationTestingBuilder
            .CreateAsync<Projects.YourApp_AppHost>([
                "YourApp:UseVolumes=false",
                "YourApp:Environment=IntegrationTest",
                "YourApp:Replicas=1"
            ]);

        _app = await builder.BuildAsync();

        using var startupCts = new CancellationTokenSource(TimeSpan.FromMinutes(10));
        await _app.StartAsync(startupCts.Token);

        using var healthCts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
        await _app.ResourceNotifications.WaitForResourceHealthyAsync("api", healthCts.Token);
    }

    public Uri GetEndpoint(string resourceName, string scheme = "https")
    {
        return _app?.GetEndpoint(resourceName, scheme)
            ?? throw new InvalidOperationException($"Endpoint for '{resourceName}' not found");
    }

    public async Task DisposeAsync()
    {
        if (_app is not null)
        {
            await _app.DisposeAsync();
        }
    }
}
csharp
using Aspire.Hosting;
using Aspire.Hosting.Testing;

public sealed class AspireAppFixture : IAsyncLifetime
{
    private DistributedApplication? _app;

    public DistributedApplication App => _app
        ?? throw new InvalidOperationException("App not initialized");

    public async Task InitializeAsync()
    {
        var builder = await DistributedApplicationTestingBuilder
            .CreateAsync<Projects.YourApp_AppHost>([
                "YourApp:UseVolumes=false",
                "YourApp:Environment=IntegrationTest",
                "YourApp:Replicas=1"
            ]);

        _app = await builder.BuildAsync();

        using var startupCts = new CancellationTokenSource(TimeSpan.FromMinutes(10));
        await _app.StartAsync(startupCts.Token);

        using var healthCts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
        await _app.ResourceNotifications.WaitForResourceHealthyAsync("api", healthCts.Token);
    }

    public Uri GetEndpoint(string resourceName, string scheme = "https")
    {
        return _app?.GetEndpoint(resourceName, scheme)
            ?? throw new InvalidOperationException($"Endpoint for '{resourceName}' not found");
    }

    public async Task DisposeAsync()
    {
        if (_app is not null)
        {
            await _app.DisposeAsync();
        }
    }
}

Pattern 2: Using the Fixture in Tests

模式2:在测试中使用夹具

csharp
[CollectionDefinition("Aspire collection")]
public class AspireCollection : ICollectionFixture<AspireAppFixture> { }

[Collection("Aspire collection")]
public class IntegrationTests
{
    private readonly AspireAppFixture _fixture;

    public IntegrationTests(AspireAppFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public async Task Application_ShouldStart()
    {
        var httpClient = _fixture.App.CreateHttpClient("yourapp");
        var response = await httpClient.GetAsync("/");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
}
See advanced-patterns.md for Endpoint Discovery, Database Testing, Playwright UI Tests, Conditional Resource Configuration, Respawn database reset, Service-to-Service Communication, and Message Queue testing patterns.
csharp
[CollectionDefinition("Aspire collection")]
public class AspireCollection : ICollectionFixture<AspireAppFixture> { }

[Collection("Aspire collection")]
public class IntegrationTests
{
    private readonly AspireAppFixture _fixture;

    public IntegrationTests(AspireAppFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public async Task Application_ShouldStart()
    {
        var httpClient = _fixture.App.CreateHttpClient("yourapp");
        var response = await httpClient.GetAsync("/");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
}
有关端点发现、数据库测试、Playwright UI测试、条件资源配置、Respawn数据库重置、服务间通信和消息队列测试模式,请参阅advanced-patterns.md

Common Patterns Summary

常见模式汇总

PatternUse Case
Basic FixtureSimple HTTP endpoint testing
Endpoint DiscoveryAvoid hard-coded URLs
Database TestingVerify data access layer
Playwright IntegrationFull UI testing with real backend
Configuration OverrideTest-specific settings
Health ChecksEnsure services are ready
Service CommunicationTest distributed system interactions
Message Queue TestingVerify async messaging
模式适用场景
基础夹具简单HTTP端点测试
端点发现避免硬编码URL
数据库测试验证数据访问层
Playwright集成结合真实后端的完整UI测试
配置覆盖测试专属设置
健康检查确保服务已就绪
服务通信测试分布式系统交互
消息队列测试验证异步消息传递

Tricky / Non-Obvious Tips

疑难技巧

ProblemSolution
Tests timeout immediatelyCall
await _app.StartAsync()
and wait for services to be healthy
Port conflicts between testsUse xUnit
CollectionDefinition
to share fixtures
Flaky tests due to timingImplement proper health check polling instead of
Task.Delay()
Can't connect to SQL ServerRetrieve connection string dynamically via
GetConnectionStringAsync()
Parallel tests interfereUse
[Collection]
attribute to run related tests sequentially
Aspire dashboard conflictsOnly one dashboard can run at a time; tests reuse the same instance
问题解决方案
测试立即超时调用
await _app.StartAsync()
并等待服务进入健康状态
测试间端口冲突使用xUnit
CollectionDefinition
共享夹具
因时序问题导致测试不稳定实现正确的健康检查轮询,而非使用
Task.Delay()
无法连接到SQL Server通过
GetConnectionStringAsync()
动态获取连接字符串
并行测试相互干扰使用
[Collection]
特性让相关测试按顺序运行
Aspire仪表板冲突同一时间只能运行一个仪表板;测试会复用同一实例

Best Practices

最佳实践

  1. Use
    IAsyncLifetime
    - Ensures proper async initialization and cleanup
  2. Share fixtures via collections - Reduces test execution time by reusing app instances
  3. Discover endpoints dynamically - Never hard-code localhost:5000 or similar
  4. Wait for health checks - Don't assume services are immediately ready
  5. Test with real dependencies - Aspire makes it easy to use real SQL, Redis, etc.
  6. Clean up resources - Always implement
    DisposeAsync
    properly
  7. Use meaningful test data - Seed databases with realistic test data
  8. Test failure scenarios - Verify error handling and resilience
  9. Keep tests isolated - Each test should be independent and order-agnostic
  10. Monitor test execution time - If tests are slow, consider parallelization
See ci-and-tooling.md for GitHub Actions setup, custom resource waiters, and Aspire CLI/MCP integration.

  1. 使用
    IAsyncLifetime
    - 确保正确的异步初始化和清理
  2. 通过集合共享夹具 - 复用应用实例以减少测试执行时间
  3. 动态发现端点 - 绝不硬编码localhost:5000之类的地址
  4. 等待健康检查 - 不要假设服务会立即就绪
  5. 使用真实依赖项测试 - Aspire让使用真实SQL、Redis等变得简单
  6. 清理资源 - 始终正确实现
    DisposeAsync
  7. 使用有意义的测试数据 - 为数据库填充真实的测试数据
  8. 测试失败场景 - 验证错误处理和恢复能力
  9. 保持测试隔离 - 每个测试应独立且与执行顺序无关
  10. 监控测试执行时间 - 如果测试缓慢,考虑并行化
有关GitHub Actions设置、自定义资源等待器和Aspire CLI/MCP集成,请参阅ci-and-tooling.md

Debugging Tips

调试技巧

  1. Run Aspire Dashboard - When tests fail, check the dashboard at
    http://localhost:15888
  2. Use Aspire CLI with MCP - Let AI assistants query real application state
  3. Enable detailed logging - Set
    ASPIRE_ALLOW_UNSECURED_TRANSPORT=true
    for more verbose output
  4. Check container logs - Use
    docker logs
    to inspect container output
  5. Use breakpoints in fixtures - Debug fixture initialization to catch startup issues
  6. Verify resource names - Ensure resource names match between AppHost and tests
  1. 运行Aspire仪表板 - 测试失败时,访问
    http://localhost:15888
    查看仪表板
  2. 结合MCP使用Aspire CLI - 让AI助手查询真实的应用状态
  3. 启用详细日志 - 设置
    ASPIRE_ALLOW_UNSECURED_TRANSPORT=true
    以获取更详细的输出
  4. 检查容器日志 - 使用
    docker logs
    检查容器输出
  5. 在夹具中使用断点 - 调试夹具初始化以捕获启动问题
  6. 验证资源名称 - 确保AppHost和测试中的资源名称匹配