.NET Aspire + xUnit을 사용한 통합 테스팅
이 스킬을 사용해야 하는 경우
다음과 같은 경우에 이 스킬을 사용하세요:
- .NET Aspire 애플리케이션에 대한 통합 테스트를 작성할 때
- 실제 데이터베이스 연결로 ASP.NET Core 앱을 테스트할 때
- 분산 애플리케이션에서 서비스 간 통신을 확인할 때
- 컨테이너에서 실제 인프라(SQL Server, Redis, 메시지 큐)로 테스트할 때
- Playwright UI 테스트와 Aspire로 조정된 서비스를 결합할 때
- 적절한 서비스 검색 및 네트워킹을 사용하여 마이크로서비스를 테스트할 때
참조 파일
- advanced-patterns.md: 엔드포인트 검색, 데이터베이스 테스팅, Playwright, 조건부 구성, Respawn, 서비스 통신, 메시지 큐
- ci-and-tooling.md: CI/CD 통합, 사용자 정의 리소스 웨이터, MCP를 사용한 Aspire CLI
핵심 원칙
- 실제 의존성 - 모의 객체가 아닌 Aspire를 통해 실제 인프라(데이터베이스, 캐시)를 사용합니다.
- 동적 포트 바인딩 - Aspire가 포트를 동적으로 할당하도록 합니다(
127.0.0.1:0). 충돌을 방지합니다. - 픽스처 생명주기 - 적절한 테스트 픽스처 설정 및 정리를 위해
IAsyncLifetime을 사용합니다. - 엔드포인트 검색 - URL을 하드코딩하지 말고, 런타임에 Aspire에서 엔드포인트를 검색합니다.
- 병렬 격리 - xUnit 컬렉션을 사용하여 테스트 병렬화를 제어합니다.
- 건강 상태 확인 - 테스트를 실행하기 전에 항상 서비스가 정상 상태가 될 때까지 기다립니다.
고수준 테스팅 아키텍처
┌─────────────────┐ ┌──────────────────────┐
│ xUnit 테스트 파일 │──사용함──────────►│ AspireFixture │
└─────────────────┘ │ (IAsyncLifetime) │
└──────────────────────┘
│
│ 시작
▼
┌───────────────────────────┐
│ DistributedApplication │
│ (AppHost에서) │
└───────────────────────────┘
│ 노출
▼
┌──────────────────────────────┐
│ 동적 HTTP 엔드포인트 │
└──────────────────────────────┘
│ 소비됨
▼
┌─────────────────────────┐
│ HttpClient / Playwright│
└─────────────────────────┘
필수 NuGet 패키지
<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>
중요: 통합 테스트용 파일 감시자 수정
각각 IHost를 시작하는 여러 통합 테스트를 실행할 때, 기본 .NET 호스트 빌더는 구성 다시 로드를 위해 파일 감시자를 활성화합니다. 이로 인해 Linux에서 파일 디스크립터 제한이 소진됩니다.
테스트 프로젝트에 테스트가 실행되기 전에 다음을 추가하세요:
// 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");
}
}
패턴 1: 기본 Aspire 테스트 픽스처 (최신 API)
using Aspire.Hosting;
using Aspire.Hosting.Testing;
public sealed class AspireAppFixture : IAsyncLifetime
{
private DistributedApplication? _app;
public DistributedApplication App => _app
?? throw new InvalidOperationException("앱이 초기화되지 않았습니다");
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($"'{resourceName}'에 대한 엔드포인트를 찾을 수 없습니다");
}
public async Task DisposeAsync()
{
if (_app is not null)
{
await _app.DisposeAsync();
}
}
}
패턴 2: 테스트에서 픽스처 사용
[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);
}
}
고급 패턴에 대해서는 advanced-patterns.md를 참조하세요: 엔드포인트 검색, 데이터베이스 테스팅, Playwright UI 테스트, 조건부 리소스 구성, Respawn 데이터베이스 재설정, 서비스 간 통신, 메시지 큐 테스트 패턴.
일반 패턴 요약
| 패턴 | 사용 사례 |
|---|---|
| 기본 픽스처 | 간단한 HTTP 엔드포인트 테스팅 |
| 엔드포인트 검색 | 하드코딩된 URL 방지 |
| 데이터베이스 테스팅 | 데이터 액세스 계층 확인 |
| Playwright 통합 | 실제 백엔드와 함께 전체 UI 테스팅 |
| 구성 재정의 | 테스트별 설정 |
| 건강 상태 확인 | 서비스가 준비되었는지 확인 |
| 서비스 통신 | 분산 시스템 상호 작용 테스트 |
| 메시지 큐 테스팅 | 비동기 메시징 확인 |
까다롭거나 명확하지 않은 팁
| 문제 | 해결책 |
|---|---|
| 테스트가 즉시 타임아웃됨 | await _app.StartAsync()를 호출하고 서비스가 정상 상태가 될 때까지 기다립니다. |
| 테스트 간 포트 충돌 | xUnit CollectionDefinition을 사용하여 픽스처를 공유합니다. |
| 타이밍으로 인한 불안정한 테스트 | Task.Delay() 대신 적절한 건강 상태 확인 폴링을 구현합니다. |
| SQL Server에 연결할 수 없음 | GetConnectionStringAsync()를 통해 연결 문자열을 동적으로 검색합니다. |
| 병렬 테스트 간섭 | [Collection] 특성을 사용하여 관련 테스트를 순차적으로 실행합니다. |
| Aspire 대시보드 충돌 | 한 번에 하나의 대시보드만 실행할 수 있습니다. 테스트는 동일한 인스턴스를 재사용합니다. |
모범 사례
IAsyncLifetime사용 - 적절한 비동기 초기화 및 정리를 보장합니다.- 컬렉션을 통해 픽스처 공유 - 앱 인스턴스를 재사용하여 테스트 실행 시간을 줄입니다.
- 엔드포인트를 동적으로 검색 - localhost:5000 등을 하드코딩하지 마십시오.
- 건강 상태 확인 대기 - 서비스가 즉시 준비되었다고 가정하지 마십시오.
- 실제 의존성으로 테스트 - Aspire를 사용하면 실제 SQL, Redis 등을 쉽게 사용할 수 있습니다.
- 리소스 정리 - 항상
DisposeAsync를 올바르게 구현합니다. - 의미 있는 테스트 데이터 사용 - 데이터베이스를 현실적인 테스트 데이터로 시드합니다.
- 실패 시나리오 테스트 - 오류 처리 및 복원력을 확인합니다.
- 테스트 격리 유지 - 각 테스트는 독립적이고 순서에 영향을 받지 않아야 합니다.
- 테스트 실행 시간 모니터링 - 테스트가 느리면 병렬화를 고려합니다.
GitHub Actions 설정, 사용자 정의 리소스 웨이터, Aspire CLI/MCP 통합에 대해서는 ci-and-tooling.md를 참조하세요.
디버깅 팁
- Aspire 대시보드 실행 - 테스트가 실패하면
http://localhost:15888에서 대시보드를 확인합니다. - MCP와 함께 Aspire CLI 사용 - AI 도우미가 실제 애플리케이션 상태를 쿼리하도록 합니다.
- 상세 로깅 활성화 - 더 자세한 출력을 위해
ASPIRE_ALLOW_UNSECURED_TRANSPORT=true를 설정합니다. - 컨테이너 로그 확인 -
docker logs를 사용하여 컨테이너 출력을 검사합니다. - 픽스처에서 중단점 사용 - 픽스처 초기화를 디버깅하여 시작 문제를 잡습니다.
- 리소스 이름 확인 - AppHost와 테스트 간에 리소스 이름이 일치하는지 확인합니다.