Azure Service Bus Testing Without the Drama Link to heading

Testing Azure Functions with Service Bus usually involves a lot of drama. You know the drill - setting up cloud resources, tests that work sometimes but not others, and that sinking feeling when something breaks in production but you can’t reproduce it locally.

And don’t get me started on shared environments. You need extra dev environments because if you share them, you get race conditions. Even worse, if you accidentally test against the QA environment that already has Azure Functions hooked to it and the QA team is actively working - well, let’s just say you’ll be getting some very unhappy messages.

I wanted to find a better way. Here’s how I figured out a testing approach that actually works without all the headaches.

The Azure Function I’m testing is straightforward - it processes orders from a Service Bus queue:

[Function("OrderProcessingFunction")]
public async Task Run(
    [ServiceBusTrigger("%QueueName%", Connection = "ServiceBusConnection")] Order order)
{
    await orderProcessingService.ProcessOrderAsync(order);
}

1. Why Not Just Use the Local Emulator? Link to heading

You might wonder, “Why not just install the Service Bus emulator locally?” Well, that works for local development, but it doesn’t solve the bigger picture:

  • Pipeline Issues - Your CI/CD pipeline needs to test too, and installing emulators on build agents is a pain
  • Team Consistency - Everyone needs the same setup, and “works on my machine” isn’t good enough
  • Isolation - Local emulators can have state issues between test runs

2. Testcontainers to the Rescue Link to heading

Testcontainers solves all these problems by running the Service Bus emulator in a Docker container that starts fresh for each test run.

Benefits:

  • Consistent - Same environment everywhere (local, CI/CD, teammate’s machine)
  • Isolated - Each test gets a clean emulator instance
  • Automated - No manual setup or cleanup needed

Here’s how the testing flow works:

graph TD A["Test Starts"] --> B["Start Testcontainer"] B --> C["Service Bus Emulator
in Docker Container"] C --> D["Create Test Queue
(order-processing-queue)"] D --> E["Send Test Message
(Order object)"] E --> F["Test Business Logic
(OrderProcessingFunction)"] F --> G["Verify Processing
(Mock assertions)"] G --> H["Cleanup Container"] H --> I["Test Complete"] C -.-> J["Fresh Instance
Every Test Run"] C -.-> K["No Cloud Resources
Required"] C -.-> L["Same Environment
Everywhere"] style C fill:#e1f5fe style J fill:#f3e5f5 style K fill:#f3e5f5 style L fill:#f3e5f5

Setup is straightforward with a Config.json file that defines your queues:

{
  "UserConfig": {
    "NamespaceConfig": [
      {
        "Name": "test-servicebus",
        "Entities": {
          "Queues": [
            {
              "Name": "order-processing-queue"
            }
          ]
        }
      }
    ]
  }
}

And a container manager class:

public class ServiceBusTestContainer : IAsyncDisposable
{
    private ServiceBusContainer? _serviceBusContainer;
    
    public string ConnectionString => _serviceBusContainer?.GetConnectionString() ?? 
        throw new InvalidOperationException("Container not started");
    
    public async Task StartAsync()
    {
        _serviceBusContainer = new ServiceBusBuilder()
            .WithImage("mcr.microsoft.com/azure-messaging/servicebus-emulator:latest")
            .WithAcceptLicenseAgreement(true)
            .WithPortBinding(5672, true)
            .WithResourceMapping("Config.json", "/ServiceBus_Emulator/ConfigFiles/")
            .Build();
        
        await _serviceBusContainer.StartAsync();
        
        // Wait for emulator to be fully ready
        await Task.Delay(TimeSpan.FromSeconds(20));
    }

    public async ValueTask DisposeAsync()
    {
        if (_serviceBusContainer != null)
        {
            await _serviceBusContainer.DisposeAsync();
        }
    }
}

3. Testing the Flow Link to heading

Here’s where it gets interesting. Instead of trying to run the entire Azure Functions runtime in tests (which is complicated), I test the flow in a simpler way:

  1. Start the container with Service Bus emulator
  2. Send a message to the queue
  3. Let the function process it (or test the business logic directly)
  4. Assert the results
[Fact]
public async Task AzureFunction_EndToEnd_ShouldProcessMessage()
{
    // Start the Service Bus Emulator
    var containerManager = new ServiceBusTestContainer();
    await containerManager.StartAsync();
    
    var connectionString = containerManager.ConnectionString;
    
    // Arrange - Create test order
    var testOrder = OrderBuilder.Create()
        .WithCustomer("Test Customer")
        .AddLaptop()
        .Build();
    
    // Test 1: Verify Service Bus connectivity
    await SendOrderToQueueAsync(connectionString, testOrder);
    var receivedOrder = await ReceiveOrderFromQueueAsync(connectionString);
    
    Assert.NotNull(receivedOrder);
    Assert.Equal(testOrder.Id, receivedOrder.Id);
    Assert.Equal(testOrder.CustomerName, receivedOrder.CustomerName);
    
    // Test 2: Verify the function logic with mocked service
    var mockService = new Mock<IOrderProcessingService>();
    var orderProcessingFunction = new OrderProcessingFunction(mockService.Object);
    
    await orderProcessingFunction.Run(testOrder);
    
    // Assert the service was called correctly
    mockService.Verify(x => x.ProcessOrderAsync(
        It.Is<Order>(o => o.Id == testOrder.Id)), Times.Once);
}

What I Learned Link to heading

This journey taught me a few things:

  1. Don’t overcomplicate the runtime - Testing business logic separately is often cleaner than trying to test the entire Azure Functions pipeline
  2. GUI tools aren’t always available - Learning to inspect things programmatically is more reliable anyway
  3. Containers make testing predictable - No more “works on my machine” issues
  4. Clean architecture pays off - Separating concerns makes everything easier to test

The final result? Tests that actually work, can run anywhere, and don’t require any cloud resources. No drama, just reliable testing.

The Code Link to heading

If you want to see the complete implementation, I’ve put it all in a GitHub repo: azure-solutions-architecture

It includes:

  • Working Azure Function with proper dependency injection
  • Integration tests using Service Bus emulator
  • Programmatic message inspection (since Service Bus Explorer doesn’t work)
  • Clean architecture with separated concerns

The README has all the details for getting it running. It’s not rocket science, just a straightforward approach that works reliably.