Testing Strategies for Microservices

Testing Strategies for Microservices

Julian Krause ยท November 20, 2025
TestingMicroservicesQuality
ArchitectureEngineering Culture

Testing microservices is fundamentally different from testing a monolith. The distributed nature of the system means you need multiple testing strategies working together to build confidence. Here is the approach I have refined over several large-scale microservice deployments.

The Testing Diamond

Forget the traditional testing pyramid for microservices. I advocate for a testing diamond: a thin layer of unit tests for pure business logic, a thick layer of integration tests, contract tests for service boundaries, and a thin layer of E2E tests for critical paths.

        /\        E2E tests (few, critical paths only)
       /  \
      /    \      Contract tests (service boundaries)
     /      \
    /--------\    Integration tests (thick layer)
     \      /
      \    /      Unit tests (pure domain logic)
       \  /
        \/

Unit Tests: Domain Logic Only

In microservices, unit tests should focus exclusively on domain logic. Testing controller-to-database flows with mocks creates brittle tests that do not catch real bugs.

public class OrderTests
{
    [Fact]
    public void Submit_WithEmptyOrder_ThrowsDomainException()
    {
        var order = Order.Create(new CustomerId("cust-123"));

        var act = () => order.Submit();

        act.Should().Throw<DomainException>()
            .WithMessage("Cannot submit an empty order.");
    }

    [Fact]
    public void AddLine_ToSubmittedOrder_ThrowsDomainException()
    {
        var order = CreateOrderWithOneLine();
        order.Submit();

        var act = () => order.AddLine(new ProductId("prod-456"), 1, Money.From(10));

        act.Should().Throw<DomainException>()
            .WithMessage("Cannot modify a submitted order.");
    }
}

Integration Tests: The Real Workhorse

Integration tests are where most of your testing budget should go. Use WebApplicationFactory with real databases via Testcontainers.

public class OrdersApiTests : IClassFixture<CustomWebApplicationFactory>
{
    private readonly HttpClient _client;

    public OrdersApiTests(CustomWebApplicationFactory factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task CreateOrder_ReturnsCreatedWithLocation()
    {
        var command = new { CustomerId = "cust-123", Lines = new[]
        {
            new { ProductId = "prod-1", Quantity = 2, UnitPrice = 29.99m }
        }};

        var response = await _client.PostAsJsonAsync("/api/orders", command);

        response.StatusCode.Should().Be(HttpStatusCode.Created);
        response.Headers.Location.Should().NotBeNull();
    }
}

public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
    private readonly MsSqlContainer _sqlContainer = new MsSqlBuilder()
        .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
        .Build();

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            services.RemoveAll<DbContextOptions<AppDbContext>>();
            services.AddDbContext<AppDbContext>(options =>
                options.UseSqlServer(_sqlContainer.GetConnectionString()));
        });
    }
}

Contract Tests: Guarding Service Boundaries

Contract tests verify that services can communicate correctly. I use Pact for consumer-driven contract testing.

// Consumer side (Order Service tests what it expects from Inventory Service)
[Fact]
public async Task GetStock_ReturnsExpectedFormat()
{
    _pactBuilder
        .UponReceiving("a request for product stock")
        .Given("product prod-123 exists with stock 50")
        .WithRequest(HttpMethod.Get, "/api/stock/prod-123")
        .WillRespond()
        .WithStatus(HttpStatusCode.OK)
        .WithJsonBody(new
        {
            productId = "prod-123",
            available = 50,
            reserved = 5
        });

    await _pactBuilder.VerifyAsync(async ctx =>
    {
        var client = new InventoryClient(ctx.MockServerUri);
        var stock = await client.GetStockAsync("prod-123");

        stock.Available.Should().Be(50);
    });
}

The contract is then shared with the provider (Inventory Service), which verifies it can fulfill the contract. This catches breaking changes before they reach production.

E2E Tests: Critical Paths Only

End-to-end tests across microservices are expensive to maintain and slow to run. Reserve them for your most critical business flows.

# docker-compose.e2e.yml
services:
  order-service:
    build: ./src/OrderService
    depends_on: [sql-server, rabbitmq]
  inventory-service:
    build: ./src/InventoryService
    depends_on: [sql-server, rabbitmq]
  sql-server:
    image: mcr.microsoft.com/mssql/server:2022-latest
  rabbitmq:
    image: rabbitmq:3-management
[Fact]
public async Task PlaceOrder_FullFlow_ReducesInventory()
{
    // Place order via Order Service
    var orderResponse = await _orderClient.PostAsJsonAsync("/api/orders", orderPayload);
    var orderId = await orderResponse.Content.ReadFromJsonAsync<Guid>();

    // Wait for async processing
    await WaitForConditionAsync(async () =>
    {
        var stock = await _inventoryClient.GetFromJsonAsync<StockDto>($"/api/stock/prod-1");
        return stock!.Available == 48; // Was 50, ordered 2
    }, timeout: TimeSpan.FromSeconds(30));
}

Key Lessons

Keep unit tests focused on domain invariants. Invest heavily in integration tests with real infrastructure. Use contract tests to protect service boundaries. Run E2E tests sparingly for critical paths. And remember that flaky tests are worse than no tests because they erode trust in the entire test suite.


Comments (3)

David Mar 10, 2026 · 17:08

Could you elaborate on this topic in a follow-up post?

Greta Mar 11, 2026 · 20:08

Interesting thought, thanks for adding that.

Hannah Mar 07, 2026 · 23:08

Exactly! I had the same thought.

Elena Mar 11, 2026 · 17:08

This is exactly what I was looking for, thank you!

Felix Mar 12, 2026 · 17:08

I have a question: does this also apply to older versions?


Leave a comment

An unhandled error has occurred. Reload ๐Ÿ—™

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.