How to automate integration testing using docker
April 17, 2025 | by codeyourassets

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:
- Docker engine
- .net core SDK 8 or newer
- An IDE which supports C# programming – a good choice is VSCode
Our application architecture
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:
Subscription | Message Filter |
---|---|
subscription.1 | ContentType = "application/json" |
subscription.2 | prop1 = "value1" |
subscription.3 | Accepts all messages |
subscription.4 | MessageId = '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 👇
RELATED POSTS
View all