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", "full@user.com");
		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 = "full@user.com", 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 = "new@user.com";
		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", "full@user.com");
			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", "full@user.com");
			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", "nonexistant@user.com");
			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.

Show Comments