Code your assets

How to automate integration testing using docker

April 17, 2025 | by codeyourassets

Integration testing with docker

In this article, we’ll look at how to automate integration testing using Docker.
By the end, you’ll be able to set up a complete test environment, including Azure Service Bus messaging—completely free.

What we’ll cover

  • Pre-requirements
  • Our application architecture
  • Integration tests vs. unit tests
  • Isolating and preparing our Rest API
  • Creating fixture for our service bus
  • Write out integration test and use our fixture
  • What we’ve learned

Pre-requirements

To begin, make sure your local setup includes the following:

Our application architecture

How to automate integration testing using docker

We made a very simple application which has the following flow:

  • The REST API exposes one POST endpoint for our integration test.
  • When the endpoint is called, it creates a basic command message. This message tells the system to say “Hello!”
  • The command is sent right away to our local Azure Service Bus.
  • The worker then processes the instructions and sends back replies
  • A simple worker listens to the Service Bus and handles these commands.
  • After processing, it sends a reply back.
  • Our integration test listens for that reply and checks that everything worked.

Even though the app is simple, this could easily be a full CQRS setup using event-driven architecture.

Additionally, while we’re focusing on Azure Service Bus, this method can also be applied to other systems like databases. Take a look at the Testcontainers documentation for more details. You’ll also find the this article’s code base linked here.

Before moving on, it’s worth looking at a few alternative approaches and their pros and cons.

Other ways we could do this

Using public cloud providers

One popular option is using cloud services like Microsoft Azure to host your test environment.

Great for beginners.
Feels like production.
No need to install much on your own machine.
Useful for testing larger systems end-to-end.
It costs money every time you run tests.
You need to securely share connection strings.
You’ll need extra code to clean up your environment.
Tests may fail if the cloud environment goes down.

Using in-memory abstractions

Another choice is to create a setup where messages pass through memory instead of real infrastructure. You can do this with frameworks like Mass Transit.

No cost to run tests.
No sensitive data to protect.
Tests are less like the real production setup.
You may miss errors until deployment.
The abstractions can be hard to build and maintain.

Integration tests vs. unit tests

To put things in context, let’s break down the difference.

Unit Tests check small pieces of code on their own. They usually fake any outside systems. This can hide serious problems until much later in the development process.

Integration Tests check how different parts of the system work together. They don’t fake the systems involved, which helps find problems earlier, before the code is deployed.

In short, while unit tests catch small issues fast, integration tests give you:

  • Interaction validation: They catch bugs when different services try to talk to each other.
  • Interface checks: They help find issues with APIs or data formats.
  • Realism: They test the system with real setups.
  • Confidence: They show that everything works from start to finish.

Isolating and preparing our REST API

At this point, let’s look at our:

using Azure.Messaging.ServiceBus;
using Microsoft.Extensions.Azure;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddAzureClients(clientBuilder =>
{
    clientBuilder.AddServiceBusClient(builder.Configuration.GetConnectionString("ServiceBus"));
    
    clientBuilder.AddClient<ServiceBusSender, ServiceBusClientOptions>(
    (_, _, provider) => provider.GetService<ServiceBusClient>()!
        .CreateSender("topic.1")).WithName("topic.1");
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.MapPost("/sendhellocommand", async (IAzureClientFactory<ServiceBusSender> senderFactory)  =>
{
    var sender = senderFactory.CreateClient("topic.1");
    var msg = new ServiceBusMessage("Say:'Hello!'"); 
    msg.MessageId = Guid.NewGuid().ToString();

    await sender.SendMessageAsync(msg);
    
    return Results.Ok(msg.MessageId);
})
.WithName("SendHelloCommand")
.WithDescription("Sends a hello command to the queue.");


app.Run();

public partial class Program { }

As you can see, we only depend on a connection string for the Azure Service Bus. The name “topic.1” is a bit odd, but we’ll cover that soon.

To isolate this in our test, we use WebApplicationFactory like this:

        var factory = new WebApplicationFactory<Program>()
        .WithWebHostBuilder(builder =>
        {
            builder.UseSetting("ConnectionStrings:ServiceBus", fixture.ConnectionString);
        });

        var apiClient = factory.CreateClient();

That’s all we need to inject the connection string. Our API is now fully testable—so let’s configure the Azure Service Bus for testing.

Creating fixture for our service bus

Before we dive in, be aware that we use xUnit for testing. Let’s quickly explain what a fixture is.

What is a Fixture?

In xUnit, a fixture allows you to reuse setup and tear-down logic across tests. Instead of repeating yourself in every method, you declare shared resources once. This makes your tests cleaner and more efficient.

For example, if several tests rely on the same Azure Service Bus instance, a fixture is the right tool.

Here’s the fixture we use for Azure Service Bus:

using System.Diagnostics;
using Testcontainers.ServiceBus;

public class AzureServicebusFixture : IAsyncLifetime
{
    private readonly ServiceBusContainer _serviceBusContainer;

    public string ConnectionString => _serviceBusContainer.GetConnectionString();

    public AzureServicebusFixture()
    {
        _serviceBusContainer = new ServiceBusBuilder()
            .WithImage("mcr.microsoft.com/azure-messaging/servicebus-emulator:latest")
            .WithAcceptLicenseAgreement(true)
            .Build();
    }

    public async Task InitializeAsync()
    {
        Debug.WriteLine("Starting Azure Service Bus emulator...");
        await _serviceBusContainer.StartAsync();
        Debug.WriteLine("Azure Service Bus emulator started.");
    }
    
    public async Task DisposeAsync()
    {
        try
        {
            Debug.WriteLine("Stopping Azure Service Bus emulator...");
            await _serviceBusContainer.StopAsync();
            Debug.WriteLine("Azure Service Bus emulator stopped.");
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error stopping Azure Service Bus emulator: {ex.Message}");
        }
    }
}

Write integration test and apply our fixture

Now, here’s how it all comes together in an integration test. We’ll keep it simple for clarity.

using System.Text;
using Azure.Messaging.ServiceBus;
using Microsoft.AspNetCore.Mvc.Testing;
using simple_worker;

namespace rest_api.tests;

public class AzureServicebusTests(AzureServicebusFixture fixture) : IClassFixture<AzureServicebusFixture>
{
    [Fact]
    public async Task SendHello_AsTheOnlyMessageInTheQueue_ShouldReceiveExactlyOneReply()
    {
        var factory = new WebApplicationFactory<Program>()
        .WithWebHostBuilder(builder =>
        {
            builder.UseSetting("ConnectionStrings:ServiceBus", fixture.ConnectionString);
        });

        var apiClient = factory.CreateClient();
        var response = await apiClient.PostAsync("/sendhellocommand", new StringContent("{}", Encoding.UTF8, "application/json"));
        var simpleWorker = new SimpleWorker(fixture.ConnectionString, "topic.1", "subscription.3");
        await simpleWorker.ReceiveAndReplyToMessagesAsync();

        var sbHelper = new ServiceBusHelper(fixture.ConnectionString, "topic.1", "subscription.3");
        var messages = await sbHelper.TryConsumeMessagesAsync();
        Assert.Equal(200, (int)response.StatusCode);
        Assert.Single(messages);
        Assert.Equal("'Hello!'", Encoding.UTF8.GetString(messages.First().Body));
        Assert.Equal(await response.Content.ReadAsStringAsync(), $"\"{messages.First().CorrelationId}\"");
    }
}

Even with all this infrastructure, the test is still short and easy to understand. You could make it cleaner by turning the REST API setup into a fixture too—but I’ll leave that as an exercise for you.

A quick note on magic strings

Before we wrap up, let’s talk about those “magic strings” like "topic.1" and "subscription.3". While not ideal, we used them here for a reason.

Specifically, the local Azure Service Bus emulator has a few built-in limitations:

  • Only one namespace
  • 50 queues/topics max
  • Four fixed subscriptions (like subscription.3)
  • No partitioning, auto-scaling, or geo-redundancy
  • No support for JMS protocol or on-the-fly management

So unless you configure things differently, you’ll need to work with its defaults. Here’s a breakdown of the predefined subscriptions:

SubscriptionMessage Filter
subscription.1ContentType = "application/json"
subscription.2prop1 = "value1"
subscription.3Accepts all messages
subscription.4MessageId = '123456' AND userProp1 = 'value1'

Learn more by checking out the official docs.

What we’ve learned

If you’ve made it this far, you now know how to create your own test environment for integration tests—without the cloud and without complex tools.

This setup is fast, clean, and doesn’t leave behind test junk. It also gives you confidence that your application works in a realistic environment.

Thanks for reading! If you liked this article, check out my guide on shipping software on a budget, and feel free to subscribe to the newsletter below 👇

Please enable JavaScript in your browser to complete this form.
Name

RELATED POSTS

View all

view all