I want to use test containers for integration tests but I cannot find any resources for setting it up in a database first approach. I have a SQL Server .sqlproj
file within my project but I cannot figure out how to use that to set up the container database. I’m hoping there’s a way to do this or maybe setup migrations only for the test container.
So far the await context.Database.EnsureCreatedAsync();
creates the tables but doesn’t create the views since it’s based on the DbContext (I’m assuming).
public class DatabaseFixture : IAsyncLifetime
{
private readonly MsSqlContainer _container;
public IServiceProvider ServiceProvider { get; private set; }
public DatabaseFixture()
{
_container = new MsSqlBuilder()
.WithImage("mcr.microsoft.com/mssql/server:latest")
.Build();
}
public async Task InitializeAsync()
{
await _container.StartAsync();
var masterConnectionString = _container.GetConnectionString();
var connectionStringBuilder = new SqlConnectionStringBuilder(masterConnectionString)
{
InitialCatalog = "master"
};
using (var connection = new SqlConnection(connectionStringBuilder.ConnectionString))
{
await connection.OpenAsync();
using (var command = new SqlCommand("CREATE DATABASE MyDatabase", connection))
{
await command.ExecuteNonQueryAsync();
}
}
connectionStringBuilder.InitialCatalog = "MyDatabase";
var serviceCollection = GetServices(connectionStringBuilder.ConnectionString);
ServiceProvider = serviceCollection.BuildServiceProvider();
using var scope = ServiceProvider.CreateScope();
var scopedServices = scope.ServiceProvider;
var context = scopedServices.GetRequiredService<MyDbContext>();
await context.Database.EnsureCreatedAsync();
}
public async Task DisposeAsync()
{
using var scope = ServiceProvider.CreateScope();
var scopedServices = scope.ServiceProvider;
var context = scopedServices.GetRequiredService<MyDbContext>();
await context.Database.EnsureDeletedAsync();
await _container.StopAsync();
}
private IServiceCollection GetServices(string connectionString)
{
var services = new ServiceCollection();
services.AddTransient<IMyServices, MyServices>();
services.AddDbContext<MyDbContext>(options => options.UseSqlServer(connectionString));
services.AddScoped(s => new MyDbContext(connectionString));
return services;
}
}
1
EnsureCreatedAsync
is able to create tables because the Entity definitions that EF generates when analyzing your database are very complete(*). It then uses those Entity definitions to do the reverse, that is create tables, as if you have an initial migration.
EnsureCreatedAsync
will not create views because EF Core does not support doing so, see e.g. https://github.com/dotnet/efcore/issues/24546 and https://github.com/dotnet/efcore/issues/26965 (both “Closed as not planned”). The generated code for a View supports capturing the results of the View, i.e. which columns it produces + their types, but it stores nothing about the actual definition (SQL code) of the View.
You’ll have to write or extract SQL code that creates your views, and run it after EnsureCreatedAsync
.
(*) -> I would not count on it being 100% complete and accurate for all features that a table could have across all DBMS-es, but it does go a long way.
For integration tests with a DB First schema, you could consider creating individual testing scenario databases suited for integration test suites. These contain the necessary schema and seed data, ready to go. Integration test suites would check for an existing DB instance at start-up, drop it if present, then restore one of the suitable backups to run the tests against. Each stub database would only contain the tables/data the test, or tests would need to interact with, and images could be spun up and expanded on when new tests needed something extra. Data population scripts were also available for spinning up new test images but the tests themselves were just responsible for executing Drops and Restore commands before running the tests.
There might be 6 backup images used across the tests, but each test suite is configured to use one image and restore to a named database instance specific for the tests in that suite. Other test suites using the same image would create and use a separately named database to avoid race conditions & stepping on toes to cause test failures. If the test passes the database can be dropped immediately on test suite completion. Failed test databases can be inspected and will get dropped on next run.
One issue with DB-first is that a lot of the important schema details like constraints, indexes, field sizes, etc. that you would define in code-first aren’t needed and can be left off in DB-first. When you build a schema using a db-first type configuration you’ll end up with something “mostly” functional but not reflecting the end production schema so integration tests might pass where you’d get failures in production due to schema differences.