Running Entity Framework Core tests against a real database

To run tests against an application that uses DbContext it is best to use a “real” database. In-memory storage doesn’t emulate a real database at all, so you aren’t going to see exceptions for referential integrity errors, unique index violations, conflicts, etc. To solve this problem, you can use one of the Db test containers from Test Containers. In this blog, I will use the .NET container for MS SQL Server.

Installation

Installation is quite simple.

  1. Ensure you have Docker running on your system.
  2. Add a reference to your test project to the NuGet package Testcontainers.MsSql.

Updating your tests’ source code

I’ll be showing you how to create a database per test. The benefits of this approach are

  • Tests don’t have to ensure they have unique data to avoid unique index violations with data left over from other tests.
  • We can continue to run our tests in parallel. We don’t need to execute them sequentially so that we can clear down the database before each test scenario.

First, we’ll need a factory class that will create a new database and give us its connection string. This class will ensure the Docker image is installed and running, create a database with a unique name, and then return a connection string we can use to access it.

internal static class TestSqlServerDatabaseFactory
{
    private static readonly Lazy<ValueTask<MsSqlContainer>> GetServerInstance = new(CreateServerInstanceAsync);
    private static int? Port;

    public static async ValueTask<string> CreateDatabaseAndGetConnectionStringAsync()
    {
        // 1: Ensure a single instance of the Docker container is running
        MsSqlContainer serverInstance = await GetServerInstance.Value;

        // 2: Ensure a database with a unique name is created
        string databaseName = "Test" + Guid.NewGuid().ToString().Replace("-", "");
        await serverInstance.ExecScriptAsync($"create database [{databaseName}]");

        // 3: Return the connection string with the new custom database name
        string connectionString =
            serverInstance
                .GetConnectionString()
                .Replace("Database=master", $"Database={databaseName}");
        return connectionString;
    }

    // Allow a custom port to connect to SQL Server through
    public static void SetPort(int port)
    {
        if (Port is not null && Port != port)
            Port = port;
    }

    private static async ValueTask<MsSqlContainer> CreateServerInstanceAsync()
    {
        MsSqlContainer result =
            new MsSqlBuilder()
                .WithAutoRemove(autoRemove: true)
                .WithExposedPort(Port ?? 54465)
                .Build();

        await result.StartAsync();
        return result;
    }
}

Next, we need to ensure every instance of our application’s DbContext uses its own database within the Docker container. To achieve this, I descend a new class from my application’s DbContext and override OnCofiguring to set the connection string, and override the constructor to ensure the database tables etc are created.

internal class TestDbContext : AppDbContextBase
{
    public DbSet<Company> Companies { get; init; }
    public DbSet<Person> People { get; init; }

    public TestDbContext()
    {
        Database.EnsureCreated();
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        string connectionString = TestSqlServerDatabaseFactory.CreateDatabaseAndGetConnectionStringAsync().Result;

        optionsBuilder.UseSqlServer(connectionString: connectionString);
        base.OnConfiguring(optionsBuilder);
    }
}

Creating a unit test

To use this new TestDbContext we can simply create an instance of it and pass it to our dependencies.

using var testDbContext = new TestDb();
var subject = new ThingToTest(testDbContext);

await subject.DoSomethingAsync();
await testDb.SaveChangesAsync();

If we need to create our own DbConnection instance to query the Db state, we can use the method testDbContext.Database.GetConnectionString(); to get the connection string we need.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *