Perfecting database cleanup
So no we run all our tests sequentially and they all share the same datastore. We have a pretty robust way to cleanup data between tests, but the main problem with it is that you won't always have an exposed endpoint that correctly cleans your data up.
This is where Respawn comes in.
Respawn
Respawn is a Nuget package that helps with resetting test databases to a clean state. Instead of deleting data at the end of the test or have a loose transaction that is being rolled back, Respawn will dynamically reset the database back to a clean state by intelligently deleting data from tables.
To use it, first let's install it.
Integrating the Respawner
We now need to integrate the Respawner instance into our tests. First we need to objects. The DbConnection that we want Respawn to use and the Respawner instance.
private DbConnection _dbConnection = default!;
private Respawner _respawner = default!;
Both of these will be initialized in the InitializeAsync
method after the CreateClient
call to ensure that any migrations have run.
Initializing the connection:
_dbConnection = new NpgsqlConnection("Server=localhost;Port=5432;Database=mydb;User ID=workshop;Password=changeme;");
Now for the Respawner we first need to open the database connection:
await _dbConnection.OpenAsync();
And then we need to create the Respawner instance with the Postgres adapter and the appropriate schemas included:
_respawner = await Respawner.CreateAsync(_dbConnection, new RespawnerOptions
{
DbAdapter = DbAdapter.Postgres,
SchemasToInclude = new[] { "public" }
});
Now everything in Postgres' public schema will be tracked for changes.
After a bit of a refactoring our Initialization looks like this:
public async Task InitializeAsync()
{
HttpClient = CreateClient();
_dbConnection = new NpgsqlConnection("Server=localhost;Port=5432;Database=mydb;User ID=workshop;Password=changeme;");
await InitializeRespawner();
}
private async Task InitializeRespawner()
{
await _dbConnection.OpenAsync();
_respawner = await Respawner.CreateAsync(_dbConnection, new RespawnerOptions
{
DbAdapter = DbAdapter.Postgres,
SchemasToInclude = new[] { "public" }
});
}
The only thing left to do is to expose a ResetDatabaseAsync
method for our tests to use:
public async Task ResetDatabaseAsync()
{
await _respawner.ResetAsync(_dbConnection);
}
And that's it!
Using reset in the tests
Now we need three main things in our tests.
First, we need to delete any _idsToDelete
functionality from our tests.
Then we need a reference to the ResetDatabaseAsync
method so we can reset the database.
This can be done by creating a field of type Func<Task>
in our class and pointing to the ResetDatabaseAsync
method in the constructor.
[Collection("Shared collection")]
public class CustomerControllerTests : IAsyncLifetime
{
private readonly HttpClient _client;
private readonly Func<Task> _resetDatabase;
public CustomerControllerTests(CustomerApiFactory customerApiFactory)
{
_client = customerApiFactory.HttpClient;
_resetDatabase = apiFactory.ResetDatabaseAsync;
}
...
The last thing left is to invoke this _resetDatabase
field from the DisposeAsync
method that should exist by implementing the IAsyncLifetime
interface.
public Task DisposeAsync() => _resetDatabase();
And that's it! All our tests now run sequentially and we no longer need to delete data manually. Respawn will do all that for us!