RavenDB is a well known open-source document-oriented databse for .NET. And of course we want to test our logic and not only locally while developing, but also our continuous integration pipeline should be able to run our tests. If you are familiar with RavenDB you can skip the next short chapter.
What is RavenDB?
Simplified RavenDB is a document store. Just imagine you would save JSON files in a database, which also allows you to query in a performant manner you are used from "traditional" databases like a SQL Server. If you are not familiar with RavenDB, on their website there is an interactive tutorial teaching you the basics.
The setup
Before we start coding, we need the current RavenDB-Server. You can find the download under https://ravendb.net/download. For your normal development or production you will always (like a SQL Server) have an active instance running. Maybe that might be the first issue we encounter when it comes to CI. We don't want to download and run a service. Also we don't want host the instance somewhere else and all running tests connect to that external service. No we would like to have everything as close as possible, but we see later how it works.
After you downloaded the package you can unzip it and depending on you just run the script which starts the server. This will automatically open your browser and guide you through some things. You can edit on which port the server should run, if it should run on a cluster and so one. For a detailed description head over to the official documentation.
I created a new database with the name StevenSample. After that we are pretty much set and can create our new solution. Therefore we create a new solution with two projects:
- A console application which holds our production code
- A test project which tests all of our code
Something like that:
RavenDBUnitTest/
+- RavenDBUnitTest/
+- RavenDBUnitTest.Tests/
In our production code we need to add the following package: RavenDB.Client
. This will give us a nice API to access our RavenDB.
The application
Personally I am a big fan of DDD and also the repository pattern. Of course everything I'll show here works without those two things, so feel free to adapt as much as you want.
In this setup you normally have a repository per aggregate and normally you load the whole object and not only parts of it. So that is where we are going with our implementation. As always the code for this example is linked at the end of this blog post. Our aggregates share a common base class called Aggregate.cs
:
public abstract class Entity
{
public string Id { get; set; }
}
And also we create our first aggregate: A blog post (like the one you are reading right now):
public class BlogPost : Aggregate
{
public string Title { get; set; }
public string Content { get; set; }
}
Please note that those DDD concepts are super simplified as I want to showcase unit-testing and not DDD.
The repository
The repository does basically the normal CRUD operations.
using System.Linq.Expressions;
using Raven.Client.Documents;
using Raven.Client.Documents.Linq;
namespace RavenDBUnitTest.Infrastructure;
public class Repository<TAggregate>
where TAggregate : Aggregate
{
private readonly IDocumentStore documentStore;
public Repository(IDocumentStore documentStore)
{
this.documentStore = documentStore;
}
public async Task<TAggregate> GetByIdAsync(string id)
{
using var session = documentStore.OpenAsyncSession();
return await session.LoadAsync<TAggregate>(id);
}
public async Task<IReadOnlyCollection<TAggregate>> GetAllAsync(
Expression<Func<TAggregate, bool>> filter = null,
Expression<Func<TAggregate, object>> orderBy = null,
bool descending = false)
{
using var session = documentStore.OpenSession();
var query = session.Query<TAggregate>();
if (filter != null)
{
query = query.Where(filter);
}
if (orderBy != null)
{
query = descending
? query.OrderByDescending(orderBy)
: query.OrderBy(orderBy);
}
return (await query.ToListAsync());
}
public async Task StoreAsync(TAggregate entity)
{
using var session = documentStore.OpenAsyncSession();
await session.StoreAsync(entity);
await session.SaveChangesAsync();
}
public async ValueTask DeleteAsync(string id)
{
using var session = documentStore.OpenAsyncSession();
session.Delete(id);
await session.SaveChangesAsync();
}
}
The repository itself just wants to have any Aggregate
type and doesn't care which specific implementation. The only missing piece right now is in the constructor. We need to pass an IDocumentStore
to it. This objects holds the information to our database.
Connecting the dots
Now the only thing missing is putting it all together as a simple console application:
using Raven.Client.Documents;
using RavenDBUnitTest;
using RavenDBUnitTest.Infrastructure;
var documentStore = new DocumentStore
{
Urls = new[] { "http://127.0.0.1:8080" },
Database = "StevenSample",
};
documentStore.Initialize();
var repository = new Repository<BlogPost>(documentStore);
var blogPost = new BlogPost { Title = "Hello World", Content = "Some text" };
await repository.StoreAsync(blogPost);
If you open the browser view you can see that in your newly created database there is now an entry
Unit tests
The magic of unit testing our RavenDB comes from the RavenDB.TestDriver
package. This package will spin up a temporary RavenDB server (locally and on our CI pipeline) so that we don't have to mock all the things. Super convenient.
Let's create some tests for our repository we created earlier. For that install that package and link the test project to the production project.
To make it work we need some "boiler plate" code which allows us to create the RavenDB instance:
using Raven.Client.Documents;
using Raven.TestDriver;
using RavenDBUnitTest.Infrastructure;
namespace RavenDBUnitTest.Tests;
public sealed class BlogPostRepositoryTests : RavenTestDriver
{
private static bool serverRunning;
private readonly IDocumentStore store;
private readonly Repository<BlogPost> sut;
public BlogPostRepositoryTests()
{
StartServerIfNotRunning();
store = GetDocumentStore();
sut = new Repository<BlogPost>(store);
}
public override void Dispose()
{
base.Dispose();
store.Dispose();
}
private static void StartServerIfNotRunning()
{
if (!serverRunning)
{
serverRunning = true;
ConfigureServer(new TestServerOptions
{
DataDirectory = "./RavenDbTest/",
});
}
}
}
The idea is the following:
- The first test in our suite will spin up the RavenDB test server
- Every other test just ignores spinning up the RavenDB server again
- We get the
IDocumentStore
viaGetDocumentStore
which comes from theRavenTestDriver
base class - Via
Dispose
we guarantee that the server will shutdown at some point in time
?? Info: Now be aware that this works perfect with xUnit, nUnit, MSTEST. There are configuratons for xUnit where you can run test cases inside one class in parallel. For that you have to introduce lock
s to make it work.
Now we can write our first test:
[Fact]
public async Task ShouldLoadBlogPost()
{
var blogPost = new BlogPost { Title = "Title", Content = "Content"};
await SaveBlogPostAsync(blogPost);
var blogPostFromRepo = await sut.GetByIdAsync(blogPost.Id);
Assert.Equal("Title", blogPostFromRepo.Title);
Assert.Equal("Content", blogPostFromRepo.Content);
}
private async Task SaveBlogPostAsync(params BlogPost[] blogPosts)
{
using var session = store.OpenAsyncSession();
foreach (var blogPost in blogPosts)
{
await session.StoreAsync(blogPost);
}
await session.SaveChangesAsync();
}
You will find some more tests in the repository linked below. If you run the tests you will find something like this:
Conclusion
I hope I could give you a small start how to unit test your beloved RavenDB without mocking or stubbing anything. Just right out of the box we can run this in a GitHub Action or on some other CI server.