Skip to content

How to create integration tests

Mogens Heller Grabe edited this page Jun 21, 2019 · 9 revisions

As you can probably imagine, writing integration tests is slightly more involved than writing ordinary unit tests. By nature, they contain more moving parts, so we need to handle some more problems, e.g. regarding timing and stuff.

Rebus is pretty nice though, because it has in-mem implementations of its transport and storage for subscriptions, sagas, and timeouts.

So, basically you can start a full bus running on in-mem persistence all the way through like this:

[Test]
public void MyTest()
{
    using (var activator = new BuiltinHandlerActivator())
    {
        var bus = Configure.With(activator)
            .Transport(t => t.UseInMemoryTransport(new InMemNetwork(), "queue-name"))
            .Subscriptions(s => s.StoreInMemory())
            .Sagas(s => s.StoreInMemory())
            .Timeouts(t => t.StoreInMemory())
            .Start();

        // exercise bus here
    }
}

If you let two bus instances share the InMemNetwork passed to UseInMemoryTransport, they can communicate, so that's how you can run realistic integration tests on your build server without any message broker available, and without the concurrency problems that that would incur.

Similarly, if you pass an InMemorySubscriberStore instance to StoreInMemory under the Subscriptions configurer, you can integration test a simple System.String pub/sub scenario like this:

[Test]
public async Task SubscriberGetsPublishedStrings()
{
    var network = new InMemNetwork();
    var subscriberStore = new InMemorySubscriberStore();

    using (var publisherActivator = new BuiltinHandlerActivator())
    using (var subscriberActivator = new BuiltinHandlerActivator())
    using (var eventWasReceived = new ManualResetEvent(initialState: false))
    {
        var publisher = Configure.With(publisherActivator)
            .Transport(t => t.UseInMemoryTransport(network, "publisher"))
            .Subscriptions(s => s.StoreInMemory(subscriberStore))
            .Start();

        subscriberActivator.Handle<string>(async message => eventWasReceived.Set());

        var subscriber = Configure.With(subscriberActivator)
            .Transport(t => t.UseInMemoryTransport(network, "subscriber"))
            .Subscriptions(s => s.StoreInMemory(subscriberStore))
            .Start();

        await subscriber.Subscribe<string>();

        await publisher.Publish("HEJ MED DIG MIN VEN");

        Assert.That(eventWasReceived.WaitOne(TimeSpan.FromSeconds(5)), Is.True, 
            "Did not receive the published event within 5 s timeout");
    }
}

in this case using a ManualResetEvent to block and wait for the subscriber to receive the published string, failing with an AssertionException if it's not received within a 5 s timeout.

While this might seem fairly straightforward, here's a little word of warning: Writing integration tests like this can quickly become complicated, so my advice is to exercise the same amount of care and discipline as you would with your production code. But that actually holds for all of your testing code – the same patterns and anti-patterns apply there too, because why wouldn't they? 😉

One of the important distinctions to make early on in my experience, is between code that would want to share between your system and your tests, and code you want to be able to swap out with test code, like the example above.

I often end up with using configuration code that looks like this:

Configure.With(activator)
    .Transport(t =>
    {
        if (Backdoor.Network != null)
        {
            t.UseInMemoryTransport(Backdoor.Network, "queue-name");
        }
        else
        {
            t.UseMsmq("queue-name");
        }
    })
    .Subscriptions(s =>
    {
        if (Backdoor.SubscriberStore != null)
        {
            s.StoreInMemory(Backdoor.SubscriberStore);
        }
        else
        {
            s.StoreInSqlServer(connectionString, "Subscriptions", isCentralized: true);
        }
    })
    .Start();

and then I have a Backdoor lying around in the project that looks somewhat like this:

internal static class Backdoor
{
    internal static InMemNetwork Network;
    internal static InMemorySubscriberStore SubscriberStore;

    public static void Reset()
    {
        Network = null;
        SubscriberStore = null;
    }

    public static void EnableTestMode()
    {
        Network = new InMemNetwork();
        SubscriberStore = new InMemorySubscriberStore();
    }
}

Combined with [InternalsVisibleTo("MyTestProject")] I can then

Backdoor.EnableTestMode();

and

Backdoor.Reset();

before and after each test in my BusIntegrationTestFixtureBase.

This way, the configuration code I use in my integration tests is the same as the configuratino code I use in my production system, save for the few lines that configure which queue and subscription storage I'm using.

Clone this wiki locally