Override DbContext in integration tests

Nick Lydon
2 min readFeb 13, 2024

Recently I wanted to test what would happen in a scheduled background job when we received an update concurrently from the API. The scheduled job deletes “unused” entities after a configured time period has elapsed, whereas the API attempts to “use” those entities by attaching them to a parent entity. Ideally the database foreign key constraints would ensure that the applications don’t leave the database in an undesired state, i.e. due to a loss of information.

I created a repo to simulate this scenario, albeit using an API with delete functionality as a replacement for the scheduled job. Here is the test and here is the guard that should prevent the bad update. The wise amongst you will know that you must treat the process as a series of atomic steps: here we have a read and then a write — which allows for another process to write an update in-between. In order to highlight and prevent this race condition I needed to prise apart the “seams” of the application.

My idea was to create a subclass of the DbContext in the test and override SaveChangesAsync. This would allow me to intercept the point where the transaction to remove the entities would be committed and insert records referencing the to-be-deleted entities, simulating a concurrent update that has occurred after the scheduled job read the “unused” records into memory.

In this commit I introduced a test to highlight the bug. The test is definitely more complex now unfortunately. In the next commit I added a simple fix (a hack really 😅) to remove all the cascade deletes, and the test passes as entity framework throws a DbUpdateException when trying to delete entities that would violate a foreign key constraint.

One of the challenges faced was being able to run the migrations. They’re associated with the specific DbContext type used in the production code, whereas I had to register the subtype to work in the tests. I ended up resolving an instance of the implementation type, applying the schema migrations, then removing that type from the IOC container and registering the subtype: lines here.

builder.ConfigureTestServices(collection =>
{
collection.BuildServiceProvider().GetRequiredService<SampleDbContext>().Database.Migrate();
collection.Remove(collection.Single(x => x.ImplementationType == typeof(SampleDbContext)));
collection.AddSampleDbContext<TestDbContext>();
collection.AddSingleton<ICommitInterceptor>(this);
});

This relies on using the AddDbContext method that allows you to specify both an interface and implementation type:

public static void AddSampleDbContext<TImplementation>(this IServiceCollection services)
where TImplementation : SampleDbContext
{
services.AddDbContext<SampleDbContext, TImplementation>((_, optionsBuilder) =>
optionsBuilder.UseSqlServer("name=ConnectionStrings:SampleDb"));
}

Resolving the context class and instantiating the database in the ConfigureTestServices method feels like a hack — it’s both synchronous and building the service provider, whilst still configuring concrete implementations. It gets the job done though, or at least until someone gives me a better solution!

--

--

Nick Lydon

British software developer working as a freelancer in Berlin. Mainly dotnet, but happy to try new things! https://github.com/NickLydon