Integration Testing with Nancy.Testing and RavenDB

The more I get to grips with NancyFX and RavenDB, the more I wish that they had been around from the start of my development career. Both frameworks lend themselves to building reliable applications that not only "just work" but can also be easily verified with automated tests.

Case in point, the following xUnit test verifies the response when a user tries to log in with an incorrect password:

[Fact]
public void LoginWithCorrectEmailAndWrongPassword()  
{
    var response = browser.Post("/login", with =>
    {
        with.HttpRequest();
        with.FormValue("Email", "[email protected]");
        with.FormValue("Password", "wrongpassword");
    });

    // Check that the response contains model validation errors                     
    Assert.False(response.Context.ModelValidationResult.IsValid);

    // Verify the error text
    Assert.Equal("IncorrectPassword", response.Context.ModelValidationResult.Errors.First().Key);
    Assert.Equal("Incorrect password, please try again", response.Context.ModelValidationResult.Errors.First().Value.First());
}

This test runs through the complete application stack and completes the entire operation in memory - no need to set up and manage external dependencies (e.g. database, email server etc).

Let's walk through the components that make this work.

The RavenDB Embeddable Datastore

RavenDB ships with an in-memory version of the database that completely replicates the functionality of the server version.

This allows us to mock out the database in the Nancy bootstrapper with a version that is local to the thread running the tests and is under the complete control of the test code.

The first step is to install the embedded version of RavenDB into your test project. This can be installed with the following nuget command:

Install-Package RavenDB.Embedded

From here, we can create a mock database and full it with test data using POCOs. Specifically, we want to do the following:

  • Creates the database and initalise its indexes
  • Insert our test data
  • Wait for all indexes to finish building

Since we want to share the same instance of the database across our entire test suite, the database has been created using the singleton pattern.

public static class EmbeddedDatabase  
{
    private static IDocumentStore store;

    public static IDocumentStore Store
    {
        // While this code is not threadsafe, it's fine for our purposes
        get
        {
            if (store == null)
            {
                store = new EmbeddableDocumentStore
                {
                    RunInMemory = true
                };

                var initialiser = new DocumentStoreInitialiser();
                initialiser.InitialiseStore(store);

                // Load up test data
                using (var session = store.OpenSession())
                {

                    var users = new List<User>
                        {
                            new User {Name = "Full User", Email = "[email protected]", Password = "correctpassword"}
                        };

                    foreach (var user in users)
                    {
                        session.Store(user);
                    }

                    session.SaveChanges();
                }

                // Wait for all indexes to complete building
                store.WaitForIndexing();
            }

            return store;
        }
    }
}

We now have a functioning, in-memory database that we can pass into Nancy.

NancyFX Testing Support

NancyFX provides a special assembly called Nancy.Testing that provides the ability to mock out external dependencies and then send requests into a "Browser" object and assert the output.

The library can be installed into our test project with the following nuget command:

Install-Package Nancy.Testing  

To begin, we need to create a TestBootstrapper, which inherits from the application's DefaultBootstrapper, allowing us to replace certain bits of the application with mock instances.

To keep things simple, if we only had an external dependency on the database, we would create the following two bootstrappers:

public class DefaultBootstrapper : DefaultNancyBootstrapper  
{
    public virtual IDocumentStore Store { get; private set; }

    public DefaultBootstrapper()
    {
        // Configure Store to point to your external RavenDB instance.  
        // For simpliclity, we'll use an embedded instance instead
        Store = new EmbeddableDocumentStore
        {
            RunInMemory = true
        };

        var initialiser = new DocumentStoreInitialiser();
        initialiser.InitialiseStore(Store);
    }

    public DefaultBootstrapper(bool testMode)
    {
        // Empty constructor used for creating a test bootstrapper
    }

    protected override void ConfigureRequestContainer(TinyIoCContainer container, NancyContext context)
    {
        base.ConfigureRequestContainer(container, context);
        container.Register(Store.OpenSession());
    }
}

and

public class TestBootstrapper : DefaultBootstrapper  
{
    private readonly IDocumentStore store;

    public TestBootstrapper() : base(true)
    {
        store = EmbeddedDatabase.Store;
    }

    public override IDocumentStore Store
    {
        get { return store; }
    }
}

This class inherits from the application's main bootstrapper but instead of calling the DefaultBootstrapper's default constructor, where the connection to the external database resides, it calls an empty constructor instead.

We're then able to pass in an instance of our in-memory database created as part of the testing process. This can be further extended to mock out dependencies of your application (e.g. your email sending code).

Putting it all Together

Since it can be expensive to instantiate the Nancy Browser class, it's best to do this once per set of tests and then let all of your tests use the same instance.

This can be done with xUnit by setting up the following fixture:

public class NancyBrowserFixture  
    {
        public Browser Browser { get; set; }
        public TestBootstrapper Bootstrapper { get; set; }

        public NancyBrowserFixture()
        {
            Bootstrapper = new TestBootstrapper();
            Browser = new Browser(Bootstrapper);
        }
    }

and then incorporating it into your tests like so:

public class AuthenticationModuleTests : IUseFixture<NancyBrowserFixture>  
{
    private Browser browser;
    private TestBootstrapper bootstrapper;

    public void SetFixture(NancyBrowserFixture fixture)
    {
        browser = fixture.Browser;
        bootstrapper = fixture.Bootstrapper;
    }

    [Fact]
    public void Register()
    {
        var email = "[email protected]";
        var fullName = "New User";

        var response = browser.Post("/register/", with =>
        {
            with.HttpRequest();
            with.FormValue("Email", email);
            with.FormValue("FullName", fullName);
            with.FormValue("Password", "secretpassword");
        });

        response.ShouldHaveRedirectedTo("/profile");

        // Check that the user has been inserted into the database
        using (var session = bootstrapper.Store.OpenSession())
        {
            var user = session.Query<User, UserIndex>().SingleOrDefault(x => x.Email == email);
            Assert.NotNull(user);
            Assert.Equal(email, user.Email);
            Assert.Equal(fullName, user.Name);
        }
    }

    [Fact]
    public void LoginWithCorrectEmailAndWrongPassword()
    {
        var response = browser.Post("/login", with =>
        {
            with.HttpRequest();
            with.FormValue("Email", "[email protected]");
            with.FormValue("Password", "wrongpassword");
        });

        Assert.False(response.Context.ModelValidationResult.IsValid);
        Assert.Equal("IncorrectPassword", response.Context.ModelValidationResult.Errors.First().Key);
        Assert.Equal("Incorrect password, please try again", response.Context.ModelValidationResult.Errors.First().Value.First());
    }

    [Fact]
    public void LoginWithCorrectEmailAndCorrectPassword()
    {
        var response = browser.Post("/login", with =>
        {
            with.HttpRequest();
            with.FormValue("Email", "[email protected]");
            with.FormValue("Password", "correctpassword");
        });

        Assert.True(response.Context.ModelValidationResult.IsValid);
        response.ShouldHaveRedirectedTo("/profile");
    }

    [Fact]
    public void LoginWithNonExistantEmail()
    {
        var response = browser.Post("/login", with =>
        {
            with.HttpRequest();
            with.FormValue("Email", "[email protected]");
            with.FormValue("Password", "wrongpassword");
        });

        Assert.False(response.Context.ModelValidationResult.IsValid);
        Assert.Equal("InvalidUser", response.Context.ModelValidationResult.Errors.First().Key);
        Assert.Equal("Could not find a user with that email", response.Context.ModelValidationResult.Errors.First().Value.First());
    }
}

Conclusion

And there we have it, a suite of xUnit tests that test your application from the request, right through the application stack into the database and back. Pretty. Damn. Cool.

From past experience, I know it can sometimes be hard to implement code off the back of posts like this (esp. if versions change or, horror of all horrors, I've inadvertently left out an important part of the puzzle). For this reason, I've created a sample application that contains everything I've just talked about.

The source can be found here (current status: Build status)

AppVeyor is providing the CI for this application free of charge - you need to pay a (reasonable) fee to build private applications. You should check them out.

Author image
About Jon Leigh
London Website

Hi, I'm Jon. I'm part of the Engineering team at Moneybox, a London based Fintech startup helping people to save and invest.

In my spare time, I've created CompareVino, a UK supermarket wine comparison website. If you like wine and buy it from a supermarket, you should definitely check it out.

If you want to get in touch, send me a message via Twitter