Article summary
Our software development team uses Entity Framework for data access in our C# application. A few months ago, we wired up unit testing for our Repository classes that were using Entity Framework.
Microsoft’s docs for unit testing against a production database system are a great resource. Initially, pulling in the exact strategy recommended by these guides (specifically, ensuring test isolation by using a test database fixture that starts a transaction before returning the EF DbContext) seemed to work perfectly. But, as the number of tests in our system grew, we started getting transient errors.
Here, I’ll highlight the specific issue we ran into. I’ll also discuss the simple fix we introduced to ensure that we were setting up — and tearing down — our tests properly.
The Problem
We started introducing repository unit testing to the project slowly (just a couple at a time to sketch out the pattern and as needed for new feature work). So, we only started seeing this issue once the number of unit tests touching the DB grew.
Essentially, every once in a while, a single test would fail with the following exceptions in the stack trace:
System.InvalidOperationException An exception has been raised that is likely due to a transient failure.
Microsoft.EntityFrameworkCore.DbUpdateException An error occurred while saving the entity changes. See the inner exception for details.
Npgsql.NpgsqlException Exception while reading from stream
System.TimeoutException Timeout during reading attempt
Most suggestions we found when searching for these exception messages involved bumping the timeout in the DB connection string. But, in our case of unit testing fairly simple operations against a tiny data set, that recommendation didn’t really apply.
We also scanned through the docs we initially referenced for setting up these tests for any warnings about configurations that might lead to these exceptions. However, nothing jumped out immediately as an issue with how our tests were wired up.
Next, we started looking through our Postgres container logs that printed while the tests were running. Once every test in the entire test class finished running, we saw a handful of these logs:
LOG: unexpected EOF on client connection with an open transaction
This note from this section of the EF testing docs was the source of the issue:
We start a transaction to make sure the changes below aren’t committed to the database, and don’t interfere with other tests. Since the transaction is never committed, it is implicitly rolled back at the end of the test when the context instance is disposed.
Implicitly rolling back the transaction at the end of the test was not enough, and we needed to explicitly close all the transactions.
A Simple Fix
Having pinpointed the issue, we just had to update our unit test classes to ensure that every transaction started is explicitly disposed of using DbContext.Database.RollbackTransaction
.
Before
public class UserRepositoryTests : IClassFixture
{
private readonly TestDatabaseFixture _testDatabaseFixture;
public UserRepositoryTests(TestDatabaseFixture fixture)
{
_testDatabaseFixture = fixture;
}
[Fact]
public void GetUser_WhenUserDoesNotExist_ReturnsNull()
{
var context = _testDatabaseFixture.CreateContext();
var subject = new UserRepository(context);
var result = subject.GetUser("mystery-user");
Assert.Null(result);
}
}
After
public class UserRepositoryTests : IClassFixture, IDisposable
{
private readonly ApplicationDbContext _context;
public UserRepositoryTests(TestDatabaseFixture fixture)
{
_context = fixture.CreateContext();
}
public void Dispose()
{
_context.Database.RollbackTransaction();
}
[Fact]
public void GetUser_WhenUserDoesNotExist_ReturnsNull()
{
var subject = new UserRepository(_context);
var result = subject.GetUser("mystery-user");
Assert.Null(result);
}
}
In this example, we also restructured our test class a bit and used the IDisposable
interface to make cleanup a bit easier.
TestDatabaseFixture.CreateContext
is responsible for beginning the transaction with context.Database.BeginTransaction
. The test class Dispose
handles tearing it down. Individual tests don’t need to worry about remembering either operation.
That’s it! I hope this saves you time if you run into a similar issue with unit testing.