Integration Testing C# with Docker: Advanced Level
\nIntegration testing is a key quality assurance stage in modern .NET applications. Using Docker allows you to create isolated, reproducible environments that are as close to production as possible. In this article, we will explore advanced techniques for integration testing C# with Docker: from setting up Testcontainers to organizing CI/CD pipelines.
\nThe material is aimed at developers already familiar with the basics of unit testing and basic Docker concepts. We will delve into container lifecycle management, working with multiple services, debugging, and optimizing test execution time.
\n\n1. Setting up Testcontainers for .NET
\nTestcontainers is a library that manages Docker containers from test code. It automatically creates, starts, and stops containers, ensuring isolation for each test.
\nTo begin, install the package into your test project:
\ndotnet add package Testcontainers\nA basic example — testing a repository with PostgreSQL:
\nusing Testcontainers.PostgreSql;\nusing Xunit;\n\npublic class UserRepositoryTests : IAsyncLifetime\n{\n private readonly PostgreSqlContainer _postgresContainer;\n\n public UserRepositoryTests()\n {\n // Configure the PostgreSQL container\n _postgresContainer = new PostgreSqlBuilder()\n .WithImage("postgres:15")\n .WithDatabase("testdb")\n .WithUsername("test")\n .WithPassword("test123")\n .WithCleanUp(true)\n .Build();\n }\n\n public async Task InitializeAsync()\n {\n // Start the container before all tests\n await _postgresContainer.StartAsync();\n }\n\n public async Task DisposeAsync()\n {\n // Stop and remove the container\n await _postgresContainer.DisposeAsync();\n }\n\n [Fact]\n public async Task AddUser_ShouldCreateUser()\n {\n // Get the connection string to the container\n var connectionString = _postgresContainer.GetConnectionString();\n var repository = new UserRepository(connectionString);\n\n await repository.AddUserAsync(new User { Name = "Test User" });\n var user = await repository.GetUserByNameAsync("Test User");\n\n Assert.NotNull(user);\n Assert.Equal("Test User", user.Name);\n }\n}\nImportant: use IAsyncLifetime to manage the container. This ensures that each test class gets a fresh database instance.
2. Multi-container Architecture with Docker Compose
\nReal applications depend on multiple services: databases, message queues, caches. Testcontainers supports Docker Compose, allowing you to run an entire infrastructure.
\nInstall the additional package:
\ndotnet add package Testcontainers.Compose\nExample docker-compose.test.yml file:
version: '3.8'\nservices:\n postgres:\n image: postgres:15\n environment:\n POSTGRES_USER: test\n POSTGRES_PASSWORD: test\n POSTGRES_DB: testdb\n redis:\n image: redis:7-alpine\nUsage in a test:
\nusing Testcontainers.Compose;\n\npublic class OrderServiceTests\n{\n private readonly DockerComposeContainer _compose;\n\n public OrderServiceTests()\n {\n _compose = new DockerComposeContainer()\n .WithComposeFilePath("docker-compose.test.yml")\n .WithEnvironment("ASPNETCORE_ENVIRONMENT", "Testing")\n .Build();\n }\n\n [Fact]\n public async Task CreateOrder_ShouldSaveToDbAndCache()\n {\n await _compose.StartAsync();\n \n var dbConnection = _compose.GetConnectionString("postgres", 5432);\n var redisConnection = _compose.GetConnectionString("redis", 6379