Integrationstests mit .NET Aspire + xUnit
Wann diese Fähigkeit verwenden
Verwenden Sie diese Fähigkeit, wenn:
- Integrationstests für .NET Aspire-Anwendungen schreiben
- ASP.NET Core-Apps mit echten Datenbankverbindungen testen
- Service-to-Service-Kommunikation in verteilten Anwendungen verifizieren
- Testen mit tatsächlicher Infrastruktur (SQL Server, Redis, Message Queues) in Containern
- Playwright-UI-Tests mit Aspire-orchestrierten Diensten kombinieren
- Microservices mit korrektem Service-Discovery und Netzwerk testen
Referenzdateien
- advanced-patterns.md: Endpunkt-Erkennung, Datenbanktests, Playwright, bedingte Konfiguration, Respawn, Service-Kommunikation, Message Queues
- ci-and-tooling.md: CI/CD-Integration, benutzerdefinierte Resource-Waiter, Aspire CLI mit MCP
Kernprinzipien
- Echte Abhängigkeiten – Verwenden Sie tatsächliche Infrastruktur (Datenbanken, Caches) über Aspire, keine Mocks
- Dynamische Port-Bindung – Lassen Sie Aspire Ports dynamisch zuweisen (
127.0.0.1:0), um Konflikte zu vermeiden - Fixture-Lebenszyklus – Verwenden Sie
IAsyncLifetimefür die ordnungsgemäße Einrichtung und Bereinigung von Test-Fixtures - Endpunkt-Erkennung – Niemals URLs fest codieren; Endpunkte zur Laufzeit von Aspire abrufen
- Parallele Isolation – Verwenden Sie xUnit-Collection-Attribute, um die Test-Parallelisierung zu steuern
- Health Checks – Warten Sie immer darauf, dass Dienste gesund sind, bevor Sie Tests ausführen
Hochrangige Testarchitektur
┌─────────────────┐ ┌──────────────────────┐
│ xUnit-Testdatei │──verwendet───► │ AspireFixture │
└─────────────────┘ │ (IAsyncLifetime) │
└──────────────────────┘
│
│ startet
▼
┌───────────────────────────┐
│ DistributedApplication │
│ (von AppHost) │
└───────────────────────────┘
│ bereitstellt
▼
┌──────────────────────────────┐
│ Dynamische HTTP-Endpunkte │
└──────────────────────────────┘
│ konsumiert von
▼
┌─────────────────────────┐
│ HttpClient / Playwright│
└─────────────────────────┘
Erforderliche NuGet-Pakete
<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>
KRITISCH: File-Watcher-Fix für Integrationstests
Wenn viele Integrationstests ausgeführt werden, die jeweils einen IHost starten, aktiviert der standardmäßige .NET Host Builder File Watcher für die Konfigurationsneuladung. Dies führt unter Linux zur Erschöpfung der File-Descriptor-Limits.
Fügen Sie dies Ihrem Testprojekt hinzu, bevor Tests ausgeführt werden:
// TestEnvironmentInitializer.cs
using System.Runtime.CompilerServices;
namespace YourApp.Tests;
internal static class TestEnvironmentInitializer
{
[ModuleInitializer]
internal static void Initialize()
{
// Deaktiviert die Konfigurationsdatei-Überwachung in Testhosts
// Verhindert die Erschöpfung von File-Deskriptoren (inotify watch limit) unter Linux
Environment.SetEnvironmentVariable("DOTNET_HOSTBUILDER__RELOADCONFIGONCHANGE", "false");
}
}
Muster 1: Basis-Aspire-Test-Fixture (Moderne API)
using Aspire.Hosting;
using Aspire.Hosting.Testing;
public sealed class AspireAppFixture : IAsyncLifetime
{
private DistributedApplication? _app;
public DistributedApplication App => _app
?? throw new InvalidOperationException("App nicht initialisiert");
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($"Endpunkt für '{resourceName}' nicht gefunden");
}
public async Task DisposeAsync()
{
if (_app is not null)
{
await _app.DisposeAsync();
}
}
}
Muster 2: Verwendung des Fixtures in Tests
[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);
}
}
Weitere Informationen finden Sie in advanced-patterns.md für Endpunkt-Erkennung, Datenbanktests, Playwright-UI-Tests, bedingte Ressourcenkonfiguration, Respawn-Datenbank-Reset, Service-to-Service-Kommunikation und Message-Queue-Testmuster.
Zusammenfassung der gängigen Muster
| Muster | Anwendungsfall |
|---|---|
| Basis-Fixture | Einfacher HTTP-Endpunkt-Test |
| Endpunkt-Erkennung | Vermeidung hartcodierter URLs |
| Datenbanktests | Überprüfung der Datenzugriffsschicht |
| Playwright-Integration | Vollständige UI-Tests mit echtem Backend |
| Konfigurationsüberschreibung | Testspezifische Einstellungen |
| Health Checks | Sicherstellen, dass Dienste bereit sind |
| Service-Kommunikation | Testen verteilter Systeminteraktionen |
| Message-Queue-Tests | Überprüfung asynchroner Nachrichtenübermittlung |
Knifflige / Nicht offensichtliche Tipps
| Problem | Lösung |
|---|---|
| Tests laufen sofort in ein Timeout | Rufen Sie await _app.StartAsync() auf und warten Sie, bis Dienste gesund sind |
| Portkonflikte zwischen Tests | Verwenden Sie xUnit CollectionDefinition, um Fixtures gemeinsam zu nutzen |
| Flaky-Tests aufgrund von Timing | Implementieren Sie ordnungsgemäßes Health-Check-Polling anstelle von Task.Delay() |
| Keine Verbindung zu SQL Server möglich | Verbindungszeichenfolge dynamisch über GetConnectionStringAsync() abrufen |
| Parallele Tests stören sich gegenseitig | Verwenden Sie das [Collection]-Attribut, um zusammengehörige Tests sequenziell auszuführen |
| Aspire-Dashboard-Konflikte | Es kann nur ein Dashboard gleichzeitig laufen; Tests verwenden dieselbe Instanz |
Best Practices
- Verwenden Sie
IAsyncLifetime– Stellt ordnungsgemäße asynchrone Initialisierung und Bereinigung sicher - Teilen Sie Fixtures über Collections – Reduziert die Testausführungszeit durch Wiederverwendung von App-Instanzen
- Entdecken Sie Endpunkte dynamisch – Niemals localhost:5000 oder ähnliches hartcodieren
- Warten Sie auf Health Checks – Gehen Sie nicht davon aus, dass Dienste sofort bereit sind
- Testen Sie mit echten Abhängigkeiten – Aspire macht es einfach, echte SQL-, Redis- usw. zu verwenden
- Räumen Sie Ressourcen auf – Implementieren Sie immer
DisposeAsynckorrekt - Verwenden Sie aussagekräftige Testdaten – Befüllen Sie Datenbanken mit realistischen Testdaten
- Testen Sie Fehlerszenarien – Überprüfen Sie Fehlerbehandlung und Resilienz
- Halten Sie Tests isoliert – Jeder Test sollte unabhängig und reihenfolgeunabhängig sein
- Überwachen Sie die Testlaufzeit – Wenn Tests langsam sind, ziehen Sie Parallelisierung in Betracht
Weitere Informationen finden Sie in ci-and-tooling.md für GitHub Actions-Setup, benutzerdefinierte Resource-Waiter und Aspire CLI/MCP-Integration.
Debugging-Tipps
- Führen Sie das Aspire-Dashboard aus – Wenn Tests fehlschlagen, überprüfen Sie das Dashboard unter
http://localhost:15888 - Verwenden Sie Aspire CLI mit MCP – Lassen Sie KI-Assistenten den tatsächlichen Anwendungsstatus abfragen
- Aktivieren Sie detailliertes Logging – Setzen Sie
ASPIRE_ALLOW_UNSECURED_TRANSPORT=truefür ausführlichere Ausgabe - Überprüfen Sie Container-Logs – Verwenden Sie
docker logs, um die Container-Ausgabe zu überprüfen - Setzen Sie Haltepunkte in Fixtures – Debuggen Sie die Fixture-Initialisierung, um Startprobleme zu erkennen
- Überprüfen Sie Ressourcennamen – Stellen Sie sicher, dass die Ressourcennamen zwischen AppHost und Tests übereinstimmen