Containerizing Our Integration Tests: Docker, SQL Server, and WireMock

In the last installment of our series on Automating Testing with NSwag, GitHub Actions, and Playwright, we laid the groundwork for generating a TypeScript client and executing Playwright tests against our API. I mentioned then that to truly run these tests reliably—especially in a CI environment—we need a consistent, isolated place to host our application during the test run. Today, we are taking that next stepping stone by building a robust testing environment using Docker and Docker Compose.

The “Holy Grail” of integration testing is ensuring your environment is perfectly reproducible. We want to test our .NET Web API against a real database, but we absolutely do not want our tests to fail because a third-party service we rely on happens to be down.

To solve this, we are going to orchestrate three distinct containers using Docker Compose:

  1. The Web API: Our actual .NET application, built and deployed inside a container.
  2. The Database: A real SQL Server instance, ensuring our Entity Framework queries execute exactly as they would in production.
  3. WireMock: A mock server that stands in for external APIs and services, ensuring our tests remain isolated and deterministic.

The Architecture of Our Test Environment

Using Docker Compose allows us to define our multi-container test environment in a single YAML file. When we spin this up, Docker creates an internal network where these containers can communicate with one another using their service names as hostnames.

Here is what our docker-compose.yml looks like:

version: '3.8'

services:
  # 1. Our .NET Web API
  webapi:
    build:
      context: .
      dockerfile: ./MyApi/Dockerfile
    ports:
      - "5000:80"
    depends_on:
      - sqldb
      - wiremock
    environment:
      - ASPNETCORE_ENVIRONMENT=Testing
      # Pointing to the SQL container
      - ConnectionStrings__DefaultConnection=Server=sqldb;Database=IntegrationTestDb;User Id=sa;Password=TestPassword123!;TrustServerCertificate=True;
      # Pointing external service calls to WireMock
      - ExternalServices__WeatherApi__BaseUrl=http://wiremock:8080

  # 2. SQL Server Database
  sqldb:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=TestPassword123!
    ports:
      - "1433:1433"

  # 3. WireMock for External Services
  wiremock:
    image: wiremock/wiremock:latest
    ports:
      - "8080:8080"
    command: ["--port", "8080", "--global-response-templating"]

Breaking Down the Containers

1. The .NET Web API Container

The webapi service builds directly from our project’s Dockerfile. The magic here happens in the environment variables. We override our connection strings and HTTP client base URLs to point to the other containers (sqldb and wiremock) instead of localhost or production endpoints. We also use depends_on to ensure the API doesn’t try to start until the database and mock server are spinning up.

2. The SQL Server Container

In-memory databases are great for fast unit tests, but they often hide provider-specific SQL issues. By pulling the official SQL Server image (mcr.microsoft.com/mssql/server), our integration tests run against the real engine. We set the EULA acceptance and a strong SA password via environment variables. During your test setup phase, Entity Framework can run its migrations against this fresh, empty database.

3. The WireMock Container

If your Web API needs to call an external payment gateway, a third-party weather API, or a legacy internal service, you don’t want those calls going out to the real internet during an integration test. By routing those external calls to the wiremock container, we can programmatically define expected requests and stubbed responses. If we don’t care about testing the actual third-party service, WireMock effectively neutralizes the dependency, preventing related failures and flaky tests.

Why This Matters

By containerizing the dependencies alongside the application, any developer can clone the repository, run docker-compose up, and instantly have a production-like environment ready for testing. There is no need to manually install SQL Server locally or configure mock endpoints on your host machine.

Furthermore, this setup is entirely portable. When we are ready to run our Playwright tests in GitHub Actions, the CI runner can execute this exact same Docker Compose file, ensuring our automated tests run in the exact same environment as they do on our local machines.

In the next post, we’ll look at how to wire this Docker Compose setup into our Playwright test execution, automatically seeding our database and configuring our WireMock stubs on the fly.

Happy Coding!

Leave a Comment

Your email address will not be published. Required fields are marked *