Robware Software by Rob

Welcome

Hello, I'm Rob. I'm a senior software engineer at On The Beach. Professionally my focus has been in backend development primarily using C#. In my spare time I spend my time riding bikes or making stuff, typically involving an Arduino.

This website is primarily an outlet for me to write about things which have been technically challenging, either in a professional or personal capacity, though not limited to that.

If you wish to get in contact, then get in touch via my LinkedIn profile.

Latest code commit

Repositorywebsite
Branchmaster
SHA9e6a13618c4a02e0c04a4913b45ee720c1c23507
MessageAdd Rybbit analytics
Timestamp19:43:23 on Monday the 20th of October 2025

Latest Blog Post

Managing transient database data in integration tests

When using unit test frameworks like xUnit the lifetime of the data you set up, for the most part, exists only in memory for the duration of that one unit test. This means that each test is run in isolation and does not affect the other tests. Or, at least, that's how they should be written.

When dealing with databases in integration tests, however, this can be tricky as the database keeps its state between test runs. This means that data which is added or manipulated remains in that state for the next test run. This can make it harder to ensure a consistent scenario each time, especially when tests are run in parallel and you cannot guarantee execution order.

Rather than trying to work around this by making sure I reference unique IDs, which can increase difficulty writing tests as the suite grows, I took the approach of using one database per test. Surprisingly, it's still pretty quick.

In this example I'll be making use of Reqnroll, which took the place of SpecFlow. I will also use a MySQL server, running in Docker.

The gist of the approach is that there is a parent class, TransientDatabaseDriver, which manages the creation and use of each temporary database and then tears it down when the test is done.

To start with, we need our TransientDatabaseDriver class:

[Binding]
public class TransientDatabaseDriver : IDisposable, IAsyncDisposable
{
	protected virtual string TestDbName => "test_db";
	protected virtual string[] SchemaFiles => [];
	public readonly string DatabaseName;

	private readonly IReqnrollOutputHelper _outputHelper;
	private readonly MySqlConnection _databaseConnection;
	private static int _counter;

	protected TransientDatabaseDriver(IReqnrollOutputHelper outputHelper)
	{
		_outputHelper = outputHelper;
		var host = Environment.GetEnvironmentVariable("TEST_DB_HOST") ?? "localhost";
		DatabaseName = TestDbName + _counter++;
		_databaseConnection = new MySqlConnection($"Server={host};Port=3306;Uid=root;Pwd=root;");
		OpenConnection();
		ExecuteNonQuery($"CREATE DATABASE IF NOT EXISTS {DatabaseName}");
		ExecuteNonQuery($"USE {DatabaseName}");
		foreach (var file in SchemaFiles)
		{
			ExecuteSqlFile(file);
		}
	}

	public void ExecuteNonQuery(string databaseIfNotExistsOtbHotelsTest, Action<MySqlCommand>? setupCommand = null)
	{
		using var command = _databaseConnection.CreateCommand();
		command.CommandText = databaseIfNotExistsOtbHotelsTest;
		setupCommand?.Invoke(command);
		var rows = command.ExecuteNonQuery();
		_outputHelper.WriteLine($"Executed SQL: {command.CommandText} - Rows affected: {rows}");
	}

	private void OpenConnection()
	{
		if (_databaseConnection.State != System.Data.ConnectionState.Open)
		{
			_databaseConnection.Open();
		}
	}

	private void CloseConnection()
	{
		if (_databaseConnection.State != System.Data.ConnectionState.Closed)
		{
			_databaseConnection.Close();
		}
	}

	[AfterScenario]
	public void CleanDatabase() => ExecuteNonQuery("DROP DATABASE IF EXISTS " + DatabaseName);

	public void Dispose()
	{
		CloseConnection();
		_databaseConnection.Dispose();
	}

	public async ValueTask DisposeAsync()
	{
		CloseConnection();
		await _databaseConnection.DisposeAsync();
	}

	protected void ExecuteSqlFile(string file) => ExecuteNonQuery(File.ReadAllText(file));
}

The work kicks off in the constructor. We set the host to localhost should we not have the TEST_DB_HOST environment variable (which is useful for CI). We then create our DB name, which uses a counter to keep it unique per test for the test session. The connection is then set up, opened, the database created and switched to. Finally any specified schema files are loaded so we can do any table setup and any data seeding we need.

Then, when the test finishes, the CleanDatabase() method is called which drops the temporary database. This mostly benefits local runs as in a CI environment the databases will be temporary anyway.

The class needs to be adorned with the Binding attribute to tell Reqnroll to load it.

Next we need to make use of it. Although the TransientDatabaseDriver isn't marked with abstract it is designed to be inherited. Unfortunately, limitations of Reqnroll mean that you can't have a non-static method marked with [AfterScenario] in an abstract class.

public class DatabaseDriver(IReqnrollOutputHelper outputHelper) : TransientDatabaseDriver(outputHelper)
{
	protected override string TestDbName => "test_db_name_override";
	protected override string[] SchemaFiles => ["table_schema.sql"];
}

This is all you need, and the overrides are optional.

This step is followed by overwriting the database setting in our application under test:

internal class HttpDriver
{
	private readonly WebApplicationFactory<Program> _factory;

	public HttpDriver(DatabaseDriver databaseDriver)
	{
		_factory = new WebApplicationFactory<Program>()
			.WithWebHostBuilder(builder =>
			{
				builder
					.UseEnvironment("Integration")
					.UseSetting("MySqlConnection:Database", databaseDriver.DatabaseName); <--- Here
			});
	}
}

Obviously the MySqlConnection:Database string should be set to whatever your application uses to determine the DB. If you have the whole connection string configured then you'd need to reconstruct that using databaseDriver.DatabaseName.

And that's it. That's all that needs to be done to enjoy transient data within your integration tests.

One more thing I'd like to highlight is the ExecuteNonQuery method. This will allow you to insert data such as part of a step within your test scenario.

[Binding]
internal class UserSteps(DatabaseDriver databaseDriver)
{
	[Given("the user exists with the following details:")]
	public void GivenTheUserExistsWithTheFollowingDetails(DataTable dataTable) {
		var users = dataTable.CreateSet<User>();
		foreach (var user in users)
		{
			ExecuteNonQuery(
				"INSERT INTO users (id, name, password) VALUES (@Id, @Name, @Password)",
				command =>
				{
					command.Parameters.AddWithValue("@Id", user.Id);
					command.Parameters.AddWithValue("@Name", user.Name);
					command.Parameters.AddWithValue("@Password", user.Password);
				});
		}
	}
}
Posted on Friday the 5th of September 2025

View more