TL;DR: Marten has a new method in V5 called ResetAllData()
that’s very handy for rolling back database state to a known point in automated tests.
I’m a big believer in utilizing intermediate level integration tests. By this I mean the middle layer of the typical automated testing pyramid where you’re most definitely testing through your application’s infrastructure, but not necessarily running the system end to end.
Now, any remotely successful test automation strategy means that you have to be able to exert some level of control over the state of the system leading into a test because all automated tests need the combination of known inputs and expected outcomes. To that end, Marten has built in support for completely rolling back the state of a Marten-ized database between tests that I’ll be demonstrating in this post.
When I’m working on a system that uses a relational database, I’m a fan of using Respawn from Jimmy Bogard that helps you rollback the state of a database to its beginning point as part of integration test setup. Likewise, Marten has the “clean” functionality for the same purpose:
public async Task clean_out_documents(IDocumentStore store)
{
// Completely remove all the database schema objects related
// to the User document type
await store.Advanced.Clean.CompletelyRemoveAsync(typeof(User));
// Tear down and remove all Marten related database schema objects
await store.Advanced.Clean.CompletelyRemoveAllAsync();
// Deletes all the documents stored in a Marten database
await store.Advanced.Clean.DeleteAllDocumentsAsync();
// Deletes all of the persisted User documents
await store.Advanced.Clean.DeleteDocumentsByTypeAsync(typeof(User));
// For cases where you may want to keep some document types,
// but eliminate everything else. This is here specifically to support
// automated testing scenarios where you have some static data that can
// be safely reused across tests
await store.Advanced.Clean.DeleteDocumentsExceptAsync(typeof(Company), typeof(User));
// And get at event storage too!
await store.Advanced.Clean.DeleteAllEventDataAsync();
}
So that’s tearing down data, but many if not most systems will need some baseline reference data to function. We’re still in business though, because Marten has long had a concept of initial data applied to a document store on its start up with the IInitialData
interface. To illustrate that interface, here’s a small sample implementation:
internal class BaselineUsers: IInitialData
{
public async Task Populate(IDocumentStore store, CancellationToken cancellation)
{
using var session = store.LightweightSession();
session.Store(new User
{
UserName = "magic",
FirstName = "Earvin",
LastName = "Johnson"
});
session.Store(new User
{
UserName = "sircharles",
FirstName = "Charles",
LastName = "Barkley"
});
await session.SaveChangesAsync(cancellation);
}
}
And the BaselineUsers
type could be applied like this during initial application configuration:
using var host = await Host.CreateDefaultBuilder()
.ConfigureServices(services =>
{
services.AddMarten(opts =>
{
opts.Connection("some connection string");
}).InitializeWith<BaselineUsers>();
}).StartAsync();
Or, maybe a little more likely, if you have some reference data that’s only applicable for your automated testing, we can attach our BaselineUsers
data set to Marten, but **only in our test harness** with usage like this:
// First, delegate to your system under test project's
// Program.CreateHostBuilder() method to get the normal system configuration
var host = Program.CreateHostBuilder(Array.Empty<string>())
// But next, apply initial data to Marten that we need just for testing
.ConfigureServices(services =>
{
// This will add the initial data to the DocumentStore
// on application startup
services.InitializeMartenWith<BaselineUsers>();
}).StartAsync();
For some background, as of V5 the mechanics for the initial data set feature moved to executing in an IHostedService
so there’s no more issue of asynchronous code being called from synchronous code with the dreaded “will it dead lock or am I feeling lucky?” GetAwaiter().GetResult()
mechanics.
Putting it all together with xUnit
The way I like to do integration testing with xUnit (the NUnit mechanics would involve static members, but the same concepts of lifetime still apply) is to have a “fixture” class that will bootstrap and hold on to a shared IHost
instance for the system under test between tests like this one:
public class MyAppFixture: IAsyncLifetime
{
public IHost Host { get; private set; }
public async Task InitializeAsync()
{
// First, delegate to your system under test project's
// Program.CreateHostBuilder() method to get the normal system configuration
Host = await Program.CreateHostBuilder(Array.Empty<string>())
// But next, apply initial data to Marten that we need just for testing
.ConfigureServices(services =>
{
services.InitializeMartenWith<BaselineUsers>();
}).StartAsync();
}
public async Task DisposeAsync()
{
await Host.StopAsync();
}
}
Next, I like to have a base class for integration tests that in this case will consume the MyAppFixture
above, but also reset the Marten database between tests with the new V5 IDocumentStore.Advanced.ResetAllStore()
like this one:
public abstract class IntegrationContext : IAsyncLifetime
{
protected IntegrationContext(MyAppFixture fixture)
{
Services = fixture.Host.Services;
}
public IServiceProvider Services { get; set; }
public Task InitializeAsync()
{
var store = Services.GetRequiredService<IDocumentStore>();
// This cleans out all existing data, and reapplies
// the initial data set before all tests
return store.Advanced.ResetAllData();
}
public virtual Task DisposeAsync()
{
return Task.CompletedTask;
}
}
Do note that I left out some xUnit ICollectionFixture
mechanics that you might need to do to make sure that MyAppFixture
is really shared between tests. See xUnit’s Shared Context documentation.