Integrationstests mit TestContainers
Wann diese Fähigkeit verwenden
Verwende diese Fähigkeit, wenn:
- Integrationstests geschrieben werden, die echte Infrastruktur benötigen (Datenbanken, Caches, Nachrichten-Warteschlangen)
- Datenzugriffsschichten gegen echte Datenbanken getestet werden
- Nachrichten-Warteschlangen-Integrationen verifiziert werden
- Redis-Caching-Verhalten getestet wird
- Mocks für Infrastrukturkomponenten vermieden werden sollen
- Sichergestellt werden soll, dass Tests in produktionsähnlichen Umgebungen funktionieren
- Datenbankmigrationen und Schemaänderungen getestet werden
Referenzdateien
- database-patterns.md: Beispiele für SQL Server, PostgreSQL und Migrationstests
- infrastructure-patterns.md: Beispiele für Redis, RabbitMQ, Multi-Container-Netzwerke, Container-Wiederverwendung und Respawn
Kernprinzipien
- Echte Infrastruktur vor Mocks – Verwende echte Datenbanken/Dienste in Containern, keine Mocks
- Testisolation – Jeder Test erhält frische Container oder frische Daten
- Automatische Bereinigung – TestContainers kümmert sich um Container-Lebenszyklus und -Bereinigung
- Schneller Start – Wiederverwende Container innerhalb derselben Klasse, wenn sinnvoll
- CI/CD-kompatibel – Funktioniert in Docker-fähigen CI-Umgebungen
- Port-Zufallsauswahl – Container verwenden zufällige Ports, um Konflikte zu vermeiden
Warum TestContainers statt Mocks?
Das Problem mit Mocking von Infrastruktur
// SCHLECHT: Mocking einer Datenbank
public class OrderRepositoryTests
{
private readonly Mock<IDbConnection> _mockDb = new();
[Fact]
public async Task GetOrder_ReturnsOrder()
{
// Dies testet kein echtes SQL-Verhalten, keine Constraints oder Performance
_mockDb.Setup(db => db.QueryAsync<Order>(It.IsAny<string>()))
.ReturnsAsync(new[] { new Order { Id = 1 } });
var repo = new OrderRepository(_mockDb.Object);
var order = await repo.GetOrderAsync(1);
Assert.NotNull(order);
}
}
Probleme: Testet keine echten SQL-Abfragen, übersieht Constraints/Indizes, gibt falsches Vertrauen, erkennt keine SQL-Syntaxfehler.
Besser: TestContainers mit echter Datenbank
// GUT: Testen gegen eine echte Datenbank
public class OrderRepositoryTests : IAsyncLifetime
{
private readonly TestcontainersContainer _dbContainer;
private IDbConnection _connection;
public OrderRepositoryTests()
{
_dbContainer = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
.WithEnvironment("ACCEPT_EULA", "Y")
.WithEnvironment("SA_PASSWORD", "Your_password123")
.WithPortBinding(1433, true)
.Build();
}
public async Task InitializeAsync()
{
await _dbContainer.StartAsync();
var port = _dbContainer.GetMappedPublicPort(1433);
var connectionString = $"Server=localhost,{port};Database=TestDb;User Id=sa;Password=Your_password123;TrustServerCertificate=true";
_connection = new SqlConnection(connectionString);
await _connection.OpenAsync();
await RunMigrationsAsync(_connection);
}
public async Task DisposeAsync()
{
await _connection.DisposeAsync();
await _dbContainer.DisposeAsync();
}
[Fact]
public async Task GetOrder_WithRealDatabase_ReturnsOrder()
{
await _connection.ExecuteAsync(
"INSERT INTO Orders (Id, CustomerId, Total) VALUES (1, 'CUST1', 100.00)");
var repo = new OrderRepository(_connection);
var order = await repo.GetOrderAsync(1);
Assert.NotNull(order);
Assert.Equal("CUST1", order.CustomerId);
Assert.Equal(100.00m, order.Total);
}
}
Siehe database-patterns.md für vollständige Beispiele zu SQL Server, PostgreSQL und Migrationstests.
Siehe infrastructure-patterns.md für Beispiele zu Redis, RabbitMQ, Multi-Container-Netzwerken, Container-Wiederverwendung und Respawn-Datenbank-Reset-Mustern.
Erforderliche NuGet-Pakete
<ItemGroup>
<PackageReference Include="Testcontainers" Version="*" />
<PackageReference Include="xunit" Version="*" />
<PackageReference Include="xunit.runner.visualstudio" Version="*" />
<!-- Datenbankspezifische Pakete -->
<PackageReference Include="Microsoft.Data.SqlClient" Version="*" />
<PackageReference Include="Npgsql" Version="*" /> <!-- Für PostgreSQL -->
<PackageReference Include="MySqlConnector" Version="*" /> <!-- Für MySQL -->
<!-- Weitere Infrastruktur -->
<PackageReference Include="StackExchange.Redis" Version="*" /> <!-- Für Redis -->
<PackageReference Include="RabbitMQ.Client" Version="*" /> <!-- Für RabbitMQ -->
</ItemGroup>
Best Practices
- Immer IAsyncLifetime verwenden – Korrekte asynchrone Einrichtung und Bereinigung
- Auf Port-Verfügbarkeit warten – Verwende
WaitStrategy, um sicherzustellen, dass Container bereit sind - Zufällige Ports verwenden – Lasse TestContainers Ports automatisch zuweisen
- Daten zwischen Tests bereinigen – Entweder frische Container verwenden oder Tabellen abschneiden (truncate)
- Container nach Möglichkeit wiederverwenden – Schneller als jedes Mal neue zu erstellen
- Echte Abfragen testen – Nicht nur Mocks testen; tatsächliches SQL-Verhalten überprüfen
- Constraints überprüfen – Fremdschlüssel, Unique-Constraints und Indizes testen
- Transaktionen testen – Rollback- und Commit-Verhalten überprüfen
- Realistische Daten verwenden – Mit produktionsähnlichen Datenmengen testen
- Bereinigung handhaben – Container immer in
DisposeAsyncentsorgen
Häufige Probleme und Lösungen
Zeitüberschreitung beim Container-Start
_container = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("postgres:latest")
.WithWaitStrategy(Wait.ForUnixContainer()
.UntilPortIsAvailable(5432)
.WithTimeout(TimeSpan.FromMinutes(2)))
.Build();
Port bereits belegt
Immer zufällige Port-Zuordnung verwenden:
.WithPortBinding(5432, true) // true = zufälligen öffentlichen Port zuweisen
Container werden nicht bereinigt
Stelle ordnungsgemäße Entsorgung sicher:
public async Task DisposeAsync()
{
await _connection?.DisposeAsync();
await _container?.DisposeAsync();
}
Tests schlagen in CI fehl, lokal aber nicht
Stelle sicher, dass CI Docker unterstützt:
# GitHub Actions
runs-on: ubuntu-latest # Docker ist vorinstalliert
CI/CD-Integration
GitHub Actions
name: Integrationstests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: .NET einrichten
uses: actions/setup-dotnet@v3
with:
dotnet-version: 9.0.x
- name: Integrationstests ausführen
run: |
dotnet test tests/YourApp.IntegrationTests \
--filter Category=Integration \
--logger trx
- name: Container bereinigen
if: always()
run: docker container prune -f
Leistungstipps
- Container wiederverwenden – Teile Fixtures über Tests in einer Sammlung
- Respawn verwenden – Daten zurücksetzen, ohne Container neu zu erstellen
- Parallele Ausführung – TestContainers behandelt Port-Konflikte automatisch
- Leichte Images verwenden – Alpine-Versionen sind kleiner und schneller
- Images cachen – Docker cached heruntergeladene Images lokal