An abstraction to help write integration tests. The idea is to have a DatabaseFixture
that gets constructed once, used for all tests and disposed when the tests are done. How this is orchestrated varies slightly depending on which test framework you use. The fixture is responsible for creating an empty database and running migrations to get the database into a "testable" state and also disposing of the database when the tests are done.
The package contains the abstract class DatabaseFixtureBase
which requires an IDatabaseAdapter
and an optional IMigrator
to be created.
This package also contains a default implementation of IDatabaseAdapter
to for use with SqlServer databases: SqlServerDatabaseAdapter
.
In this example we will assume you have created a migrator class (FixtureMigrator
). See instructions below on how to implement a migrator class using ADatabaseMigrator, EF Core or FluentMigrations.
- Create your fixture class
public class DatabaseFixture() : DatabaseFixtureBase(
new SqlServerDatabaseAdapter(ConnectionFactory),
new FixtureMigrator()), IAsyncLifetime
{
private static SqlConnection ConnectionFactory(string connectionString) => new(connectionString);
}
- Create a collection definition using your fixture (xUnit specific). The purpose for this is to only have your fixture created once and reused for all your integration tests
[CollectionDefinition("DatabaseIntegrationTest")]
public class DatabaseCollectionDefinition : ICollectionFixture<DatabaseFixture>
{
// This class has no code, and is never created. Its purpose is simply
// to be the place to apply [CollectionDefinition] and all the
// ICollectionFixture<> interfaces.
}
[Collection("DatabaseIntegrationTest")]
public abstract class DatabaseTest : IAsyncLifetime
{
public DatabaseTest(DatabaseFixture fixture)
{
Fixture = fixture;
Dude = new Dude().EnableAutomaticForeignKeys();
}
public DatabaseFixture Fixture { get; }
public Dude Dude { get; }
private static Respawner Respawner { get; set; }
public async Task InitializeAsync()
{
Respawner ??= await Respawner.CreateAsync(Fixture.ConnectionString, new RespawnerOptions
{
// Note that different migration libraries use different tables to store migration history,
// we want to exclude this table from Respawns reset
TablesToIgnore = [new(FixtureMigrator.VersioningTable)],
});
}
public Task DisposeAsync() => Respawner.ResetAsync(Fixture.ConnectionString);
}
- Go ahead and write your first integration test (using Dapper for example)
public class Mytest : DatabaseTest
{
public Mytest(DatabaseFixture fixture) : base(fixture) { }
[Fact]
public async Task TestChangeDepartment()
{
// Arrange
using var connection = Fixture.CreateNewConnection();
await Dude
.Insert("Department", new { Id = 1, Name = "HR" })
.Insert("Department", new { Id = 2, Name = "IT" })
.Insert("Employee", new { Id = 1, Name = "Jane Doe", DepartmentId = 1 })
.Go(connection);
var handler = new ChangeDepartmentHandler(connection);
var command = new ChangeDepartment(employee: 1, newDepartment: 2);
// Act
await handler.Handle(command);
// Assert
var departmentName = await connection.QuerySingleAsync<string>(@"
SELECT Department.Name
FROM Employee
INNER JOIN Department ON Department.Id = Employee.DepartmentId");
departmentName.ShouldBe("IT");
}
}
ADatabaseFixture makes it easy for you to plug in your own schema migration logic. Here are some examples of how to interface with a few migration libraries. All you need is to create a class that implements the interface IMigrator
.
Example using ADatabaseMigrator
This requires referencing the package ADatabaseMigrator
public class FixtureMigrator : ADatabaseFixture.IMigrator
{
public const string VersioningTable = "SchemaVersionJournal";
public async Task MigrateUp(string connectionString, CancellationToken? cancellationToken)
{
using var connection = new SqlConnection(connectionString);
connection.Open();
// See documentation at https://github.com/carl-berg/ADatabaseMigrator for how to create a ADatabaseMigrator migrator class
await new MyDatabaseMigrator(connection).Migrate(cancellationToken);
}
}
Example using EfCore Migrations
This requires referencing the package Microsoft.EntityFrameworkCore.SqlServer
public class FixtureMigrator : ADatabaseFixture.IMigrator
{
public const string VersioningTable = "__EFMigrationsHistory";
public async Task MigrateUp(string connectionString, CancellationToken? cancellationToken)
{
using var connection = new SqlConnection(connectionString);
using var dbContext = MyDbContext(new DbContextOptionsBuilder<MyDbContext>().UseSqlServer(connection).Options);
connection.Open();
await dbContext.Database.MigrateAsync(cancellationToken ?? default);
}
}
Example using FluentMigrator
This requires referencing the package FluentMigrator.Runner
public class FixtureMigrator : ADatabaseFixture.IMigrator
{
public const string VersioningTable = "VersionInfo";
public Task MigrateUp(string connectionString, CancellationToken? cancellationToken)
{
using var serviceProvider = new ServiceCollection()
.AddFluentMigratorCore()
.ConfigureRunner(rb =>
{
rb.ScanIn(typeof(FixtureMigrator).Assembly).For.Migrations();
rb.WithGlobalConnectionString(connectionString);
rb.AddSqlServer2016();
})
.BuildServiceProvider(false);
using var scope = serviceProvider.CreateScope();
var runner = scope.ServiceProvider.GetRequiredService<IMigrationRunner>();
runner.MigrateUp();
return Task.CompletedTask;
}
}