Integration Testing C# with Docker: How to Get Rid of Environment Problems
Jeder C#-Entwickler kennt diese Situation: Tests laufen lokal einwandfrei, scheitern aber auf dem CI-Server. Der Grund sind meist Unterschiede in der Umgebung: Datenbankversion, Redis-Einstellungen, fehlende Testdienste. Integrationstests mit C# und Docker lösen dieses Problem ein für alle Mal. In diesem Artikel sehen wir uns an, wie man eine isolierte Testumgebung mit Containern einrichtet, echte Integrationstests schreibt und sie in eine CI/CD-Pipeline integriert.
Warum Docker das perfekte Werkzeug für C#-Integrationstests ist
Integrationstests prüfen die Interaktion zwischen Systemkomponenten: Anwendung + Datenbank, API + Cache, Microservices untereinander. Ohne Docker müsste man:
- PostgreSQL, SQL Server, Redis auf jedem Entwicklerrechner installieren
- Den CI-Server mit denselben Versionen konfigurieren
- Daten zwischen Testläufen bereinigen
Docker löst all diese Aufgaben. Sie beschreiben die Infrastruktur als Code (Dockerfile, docker-compose.yml), und sie wird auf jedem System identisch bereitgestellt. Für C# (.NET) ist dies besonders relevant, da Microsoft Containerisierung aktiv unterstützt: offizielle .NET SDK-Images, SQL Server in einem Linux-Container, Integration mit Azure DevOps.
Projekt-Setup: Testcontainers + xUnit
Für Integrationstests mit C# und Docker verwenden wir die Bibliothek Testcontainers (verfügbar auf NuGet). Sie verwaltet den Container-Lebenszyklus automatisch: startet vor Tests, stoppt nach Abschluss.
Schritt 1. Pakete installieren
Erstellen Sie ein neues Projekt für Tests oder öffnen Sie ein bestehendes. Fügen Sie NuGet-Pakete hinzu:
dotnet add package Testcontainersdotnet add package Testcontainers.PostgreSqldotnet add package xunitdotnet add package FluentAssertionsSchritt 2. Basisklasse für Tests mit Containern
Erstellen wir eine abstrakte Klasse, die PostgreSQL vor dem Ausführen der Tests startet:
using Testcontainers.PostgreSql;using Xunit;
public abstract class IntegrationTestBase : IAsyncLifetime{ private readonly PostgreSqlContainer _postgresContainer;
protected string ConnectionString => _postgresContainer.GetConnectionString();
protected IntegrationTestBase() { _postgresContainer = new PostgreSqlBuilder() .WithImage("postgres:15-alpine") .WithDatabase("testdb") .WithUsername("test") .WithPassword("test123") .Build(); }
public async Task InitializeAsync() { await _postgresContainer.StartAsync(); }
public async Task DisposeAsync() { await _postgresContainer.StopAsync(); }}Die Klasse implementiert IAsyncLifetime aus xUnit — der Container wird vor dem ersten Test gestartet und nach dem letzten gestoppt. Die Methode GetConnectionString() gibt den dynamischen Port zurück, den Docker dem Container zugewiesen hat.
Schritt 3. Einen Integrationstest schreiben
Schreiben wir nun einen Test, der die Arbeit des Repositorys mit einer echten Datenbank überprüft:
public class UserRepositoryTests : IntegrationTestBase{ [Fact] public async Task CreateUser_Should_SaveToDatabase() { // Arrange await using var connection = new NpgsqlConnection(ConnectionString); await connection.OpenAsync(); var repository = new UserRepository(connection); var user = new User { Name = "Ivan", Email = "ivan@example.com" };
// Act var userId = await repository.CreateAsync(user);
// Assert userId.Should().BeGreaterThan(0); var savedUser = await repository.GetByIdAsync(userId); savedUser.Should().NotBeNull(); savedUser.Name.Should().Be("Ivan"); savedUser.Email.Should().Be("ivan@example.com"); }}