Seldom will you get worse documentation than trying to use the Azure ServiceBus emulator in a real-world scenario. To spare you the frustrations, here's how I translated their docker-compose.yaml reference implementation to something you can actually use as a test fixture in a .NET project.
First, we need 2 containers, one for MSSQL, one for the ServiceBus emulator. ServiceBus emulator uses MSSQL as a backing storage solution. To make things neater, we'll also follow the example docker-compose.yaml file and add an internal network between the two so they don't need to communicate directly over the host's network.
ServiceBus emulator's official
docker-compose.yamlexample file usesazure-sqledge, but it makes no sense to use since it is deprecated and is being phased out. The MSSQL server image works just fine.
private readonly INetwork _network;
public MyFixture()
{
_network = new NetworkBuilder.Build();
}Then, we need the MSSQL server instance. Easiest way to get going is to add the package Testcontainers.MsSql. Unless you want to manually configure it with health checks, etc. Yeah, didn't think so.
Let's use MsSqlBuilder to define the container:
private readonly MsSqlContainer _sqlContainer;
public MyFixture()
{
// ...
_sqlContainer = new MsSqlBuilder()
// Set MSSQL_SA_PASSWORD (required, needs to be overly complicated)
.WithPassword("SuperS3CRETP455w0rd!")
// Sets the hostname of the container inside the network, so we can simply use 'mssqlserver' instead of an IP or container-name
.WithNetworkAliases("mssqlserver")
// Accepts the EULA, Microsoft legal stuff
.WithEnvironment("ACCEPT_EULA", "Y")
// Assigns the container to the network we built earlier
.WithNetwork(_network)
// Port forward the port for debugging. Will be randomized host port unless we are debugging
.WithPortBinding(1433, !Debugger.IsAttached)
.Build();
}Apparently implementing SDK support for setting up/disposing queues is just not supported. So we can't use the ServiceBusAdministrationClient to setup/dispose manually between tests. So we have to define the initial queues in a config.json file and then manually run SQL queries to drop all messages.
I don't like to embed random files into my test suite or mount files on disk I might not know if I have the correct permissions to on a build agent. If I can keep it within code, that's what I do. So to specify the config.json, I like to just write it in-line:
public MyFixture()
{
var config =
"""
{
"UserConfig": {
"Namespaces": {
"Name": "my-namespace",
"Queues":[{
"Name": "my-queue",
"Properties": {}
}
]
},
"Logging": {
"Type": "Console"
}
},
}
""";
}Then, we'll define the ServiceBus emulator:
private readonly IContainer _serviceBusEmulatorContainer;
public MyFixture()
{
_serviceBusEmulatorContainer = new ContainerBuilder()
// Set the hostname in the network
.WithNetworkAliases("sb-emulator")
// Set the image to the latest servicebus-emulator image (or use a custom tag if need be).
.WithImage("mcr.microsoft.com/azure-messaging/servicebus-emulator:latest")
// I don't care about always pulling the latest, up to you
.WithImagePullPolicy(PullPolicy.Missing)
// Bind the servicebus client port. Random host port if debugger is not runnung.
.WithPortBinding(5672, !Debugger.IsAttached)
// Bind the HTTP port. Random host port if debugger is not running.
.WithPortBinding(5300, !Debugger.IsAttached)
// Set the MSSQL Server endpoint. This points to the network alias in our MSSQL container.
.WithEnvironment("SQL_SERVER", "mssqlserver")
// Set the MSSQL_SA_PASSWORD to allow admin access the MSSQL server instance.
// This way the emulator may create databases and tables.
.WithEnvironment("MSSQL_SA_PASSWORD", "SuperS3CRETP455w0rd!")
// Accept Microsoft's EULA
.WithEnvironment("ACCEPT_EULA", "Y")
// Imagine if they actually put this in the official docs.
// This makes sure the container isn't seen as ready until the /health endpoint returns status code 200.
// https://github.com/Azure/azure-service-bus-emulator-installer/blob/main/ServiceBus-Emulator/Schema/HealthCheckApiSchema.md
.WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(x => x
.ForPath("/health")
.ForPort(5300)
.ForStatusCode(HttpStatusCode.OK)))
// Wait unitl MSSQL container is ready
.DependsOn(_sqlContainer)
// Bind the in-line config JSON to the startup configuration-file.
.WithResourceMapping(Encoding.UTF8.GetBytes(config), "/ServiceBus_Emulator/ConfigFiles/Config.json")
// Attach the container to the network.
.WithNetwork(_network)
.Build();
}We need to start infrastructure in the following order:
// xUnit's IAsyncLifetime initialize method (called once per test class before tests run).
public async Task InitializeAsync()
{
_sqlContainer.StartAsync();
_serviceBusEmulatorContainer.StartAsync();
}And kill them in the opposite order:
// xUnit's dispose method (called once per test class once all tests finish).
public async Task DisposeAsync()
{
_serviceBusEmulatorContainer.StopAsync();
_sqlContainer.StopAsync();
}But what about potentially lingering messages between tests? Let's expose a ResetAsync() method that the test class can call between each tests to clean up:
// Call this between tests.
public async Task ResetAsync()
{
// Delete all messages in the MessagesTable,
await _sqlContainer.ExecScriptAsync("TRUNCATE TABLE SbMessageContainerDatabase00001.dbo.MessagesTable;");
// Delete all scheduled messages in the ScheduledMessagesTable.
await _sqlContainer.ExecScriptAsync("TRUNCATE TABLE SbMessageContainerDatabase00001.dbo.ScheduledMessagesTable;");
// Delete all message bodies in the BodiesTable.
await _sqlContainer.ExecScriptAsync("TRUNCATE TABLE SbMessageContainerDatabase00001.dbo.BodiesTable;");
// Delete all message references in the MessageReferencesTable.
await _sqlContainer.ExecScriptAsync("TRUNCATE TABLE SbMessageContainerDatabase00001.dbo.MessageReferencesTable;");
}NOTE: This is a shot in the dark. It seems to work for me, but there's ZERO documentation on how messages are handled under-the-hood. You could also drop the ENTIRE ServiceBus emulator and MSSQL Server containers between tests if you don't value your time.
Use IClassFixture<> to reference your created fixture.
public sealed class MyTests : IClassFixture<MyFixture>, IAsyncLifetime
{
private readonly MyFixture _fixture;
public MyTests(MyFixture fixture)
{
_fixture = fixture;
}
public async Task InitializeAsync() => Task.CompletedTask;
public async Task DisposeAsync()
{
// Remove all messages between tests
await _fixture.ResetAsync();
}
}Now this should spin up your ServiceBus emulator with backing storage each time the test class is run. Between each test, the messages are cleaned up, and finally after all tests have been run in the test class, it will dispose the containers.
You'll need to connect to your ServiceBus emulator instance, obviously. Simply expose a connection string from the fixture like so:
public class MyFixture
{
// The connection string to servicebus emulator.
// Finds the host-mapped port (which could be random at runtime).
public string ConnectionString => $"Endpoint=sb://localhost:{_serviceBusEmulatorContainer.GetMappedPublicPort("5672")};SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;";
}NOTE: If you run this test inside an existing Docker container with the
--volume /var/run/docker.sock:/var/run/docker.sockapproach, you'll find trouble with the connectionstrings' endpoint pointing tolocalhosthere. Consider replacing withhost.docker.internalinstead and then add the following to your system's and your build agent's host files if it's not already set:
127.0.0.1 host.docker.internalIt makes everything consistent. Otherwise, you'll have to find some other way to connect to the ServiceBus emulator inside an existing container.
Now, you can get the connectionstring from your test class and do your thing:
[Fact]
public async Task Sends_Message()
{
// Arrange
var client = new ServiceBusClient(_fixture.ConnectionString);
var queueSender = client.CreateSender("my-queue");
// Act
await queueSender.SendMessageAsync(
new ServiceBusMessage("Microsoft should spend more time on its development tools.")
{
Subject = "my-subject"
});
await queueSender.CloseAsync();
var queueReceiver = client.CreateReceiver("my-queue");
var messages = await queueReceiver.PeekMessagesAsync(1);
var message = messages.FirstOrDefault();
await queueReceiver.CloseAsync();
await client.DisposeAsync();
// Assert
Assert.NotNull(message);
Assert.IsEqual("my-subject", message.Subject);
}NOTE: Obviously, you can make this much nicer by also handling the lifecycle of the
ServiceBusClientand receivers/senders in your test class. This is simply to show how you can quickly get something running in a single test.