Testing Your RavenDB Indexes

Tags: RavenDB, Testing

In this post I am going to go through how to test your RavenDB indexes. For those who don't know RavenDB yet, it is a high performance .NET document database. I'm not going to go into the specifics here, but focus on testing the indexing.

RavenDB works with documents, which are your main data block (for lack of a better word). In order to query over documents you will have to index them, as described in the documentation. I am by no means a RavenDB expert, so please do read the official documentation.

Even though indexes are created using standard LINQ syntax, there are some subtleties that can annoy you when it comes to more complex map/reduce indexes. These are things like the reduce operation not completing because some LINQ operators (or combinations of them) are not supported or likewise. Because I am not an expert, I needed a way to verify that my indexes were working as I expected, so I needed to test them. (Yes I know, you are supposed to test all your code).

As I mentioned, there are some subtleties that will trip you sometimes, so you also have to validate that the database engine itself will accept your index. You will need to create it in an actual database to be sure. Normally I run my RavenDB database from CloudBird, but I don't want to run my tests against my live databases.

Fortunately RavenDB is extremely testable and can be created as an in-memory database, which is ideal for setting up and tearing down your test harness. With my in-memory database I can create the index, store a document and query the index as if I were running against an actual production system - except that it's lightning fast, so will not hold up your tests. To use an in-memory documentstore you will need the RavenDB.Embedded NuGet package.

Setting Up Your Test

To run your test you will need to set up your document store. In the code below, you can see that it is simply a question of creating an instance of an EmbeddableDocumentStore and setting it to run in memory, and then initializing it.

var store = new EmbeddableDocumentStore { RunInMemory = true };
store.Initialize();

The second setup step is a little more complicated and involves setting up your index. In the code sample I opted for setting up a TestCaseSource (NUnit syntax), which passes in the type of the index to the test method. The I simply rely on the Activator to create an instance using the default constructor (which I assume is going to be the default scenario for indexes). Since custom indexes inherit from AbstractIndexCreationTask, I can safely cast to that, which is useful when I have to actually execute the index.

Next comes the step of creating a document to store in your database, and later query for, so that you can check that your index works. Again we can use reflection to help us determine which document to create. If your index inherits from the generic version of AbstractIndexCreationTask, you can get the generic parameters, which will give you the type of document to create. If the index is a simple Map index, it will inherit from AbstractIndexCreationTask<T>, whereas Map/Reduce indexes inherit from AbstractIndexCreationTask<T, TReduce>. In both cases T represents the document that is saved to the documentstore and TReduce is the result type of the Map/Reduce operation. From this we can see that we will need to create a document of Type T (the first generic parameter) and store it. In the sample below, I have not shown how to create an instance of the document as that will depend on your document, but it should be a trivial case of populating some document properties. (If it isn't you should review your documents).

To query we take the last generic parameter, which is TReduce, or simply T if it is a simple Map index. Once we know what type to query for we can use the standard RavenDB query - except that this is a generic method. But we can invoke the actual query operation inside its own generic method and invoke it through reflection, since we know the type of document we are querying for as well as the index we are querying. In the code sample, you can see that the CanGetQueryResult method is a generic method, which matches the generic parameters for the Query<,> method. Invoking it becomes a matter of getting a reference to the MethodInfo and creating a generic method from that. That is what this line does:

var queryMethod = GetType()
		.GetMethod("CanGetQueryResult", BindingFlags.NonPublic | BindingFlags.Instance)
		.MakeGenericMethod(reduceType, indexType);

When you invoke the generic version of the CanGetQueryResult method that you created and pass the IDocumentSession that you wish to query, it will return a result saying whether querying the index returned a result. You can then assert that the index actually returned a result, meaning that the index works.

This is the test code:

[TestCaseSource(typeof(IndexProvider))]
public void CanQueryIndex(Type indexType)
{
	var store = new EmbeddableDocumentStore { RunInMemory = true };
	store.Initialize();
	var index = (AbstractIndexCreationTask)Activator.CreateInstance(indexType);
	store.ExecuteIndex(index);

	var types = index.GetType().BaseType.GetGenericArguments();
	var documentType = types.First();
	var reduceType = types.Last();
	var document = CreateDocument(documentType);

	using (var session = store.OpenSession())
	{
		session.Store(document);
		session.SaveChanges();
	}

	using (var querySession = store.OpenSession())
	{
		var queryMethod = GetType()
			.GetMethod("CanGetQueryResult", BindingFlags.NonPublic | BindingFlags.Instance)
			.MakeGenericMethod(reduceType, indexType);
		var result = (bool)queryMethod.Invoke(this, new object[] { querySession });
		Assert.True(result);
	}
}

private bool CanGetQueryResult<T, TIndex>(IDocumentSession session)
	where TIndex : AbstractIndexCreationTask, new()
{
	var results = session.Query<T, TIndex>()
		.Customize(x => x.WaitForNonStaleResults())
		.Any();
	return results;
}

A final thing to note is that the CanGetQueryResult method also includes a customization of the query to allow the index to finish it's indexing operation after inserting the document. This is necessary since indexes are only eventually consistent, and you don't want your test to fail because of a timing issue with the indexing operation

I haven't gone into how to set up your TestCaseSource to pass the index types to the test, but you can find tutorials about that if you bing Google. If you are using XUnit you can use a PropertyData attribute to achieve the same result.

Latest Tweets