Article summary
In a .Net Core project, unit testing repositories with an in-memory database can be as straightforward as testing other classes. Mock the dependencies (usually the database context), inject them, and then assert on what happens. However, if you’re using the DbContextFactory
class to create DbContext
instances for your repository methods, you may run into problems without some additional prep.
Problem
If your repository has a shared context across the class, this is easy. Simply create the context in your unit test, inject it, and then use the context to assert what happened. But with the factory pattern, it’s not as simple. Injecting the factory itself won’t give you access to the created DbContext being used in the method being tested. So, how can we ensure we can read from the context that our repository creates for itself and then throws away without compromising the pattern we use in the repository’s implementation?
In short, we can create a new mock DbContextFactory
class for our tests. It needs to be able to do three things for us:
- Create a
DbContext
instance when asked - Ensure that the created DbContext is shared between the implementation and the test
- Prevent the repository from disposing of the context once it’s done with it
Solution
Here’s how we can set that up.
Step 1
Our new InMemorySpinPostContextFactory
class needs to implement the IDbContextFactory
interface, the same interface as the _dbContextFactory
variable in our repository class. The method we need to implement to satisfy our first condition is CreateDbContext
. To do this, we can create a new context when it’s called, and return it. This gives us a new context when we call it, but won’t satisfy our other requirements, since the context is new every time.
public class InMemorySpinPostContextFactory : IDbContextFactory<SpinPostContext>
{
public InMemorySpinPostContextFactory(string databaseName = "InMemoryTest")
{
}
public SpinPostContext CreateDbContext()
{
DbContextOptions options = new DbContextOptionsBuilder()
.UseInMemoryDatabase(databaseName)
.Options;
return new SpinPostContext(options);
}
}
Step 2
To ensure we can share the context between our tests and repository, we can make some small adjustments. We can move the context creation from our CreateDbContext
method into our constructor. We’ll assign this value to a new private property in the class. Then, in our CreateDbContext
method, we can simply return the value of the class-level context. This achieves our second requirement, because now when the repository creates a new context when being run as part of a test, it’ll actually just be the same instance that we create when we initialize the factory.
public class InMemorySpinPostContextFactory : IDbContextFactory<SpinPostContext>
{
private readonly SpinPostContext _dbContext;
public InMemorySpinPostContextFactory(string databaseName = "InMemoryTest")
{
DbContextOptions options = new DbContextOptionsBuilder()
.UseInMemoryDatabase(databaseName)
.Options;
_dbContext = new SpinPostContext(options);
}
public SpinPostContext CreateDbContext()
{
return _dbContext;
}
}
Step 3
There’s one more requirement we need to fulfill, which becomes obvious if you run a test with our mock factory in its current state. As it stands, we’ll get a System.ObjectDisposedException
if we try to use the context for assertions at the end of our test. This is because, in our implementation, we’re “using” the context, which ensures that when the method is finished, the DbContext will be disposed of. To circumvent this, we can make one more set of changes to our mock factory class.
The change to our factory is quite simple. Because the context is being disposed of, we need a new context class that still represents our database but can ignore any calls to be disposed of. So in our constructor, we’ll change the assignment of _dbContext
to use a new TestSpinPostContext
instead. This class doesn’t exist yet, so we’ll make one.
Making the Class
All that TestSpinPostContext
needs to do is implement SpinPostContext
, and the IDisposable
interface. The former so that it can act as a SpinPostContext
when needed (like in our CreateDbContext
method), and the latter so that we can circumvent the disposal of the class when testing. Now all we need to do is implement an empty Dispose
method, which when called will now do nothing.
public class InMemorySpinPostContextFactory : IDbContextFactory<SpinPostContext>
{
private readonly TestSpinPostContext _dbContext;
public InMemorySpinPostContextFactory(string databaseName = "InMemoryTest")
{
DbContextOptions options = new DbContextOptionsBuilder()
.UseInMemoryDatabase(databaseName)
.Options;
_dbContext = new TestSpinPostContext(options);
}
public SpinPostContext CreateDbContext()
{
return _dbContext;
}
}
public class TestSpinPostContext : SpinPostContext, IDisposable
{
public TestSpinPostContext(DbContextOptions options) : base(options)
{
}
public new void Dispose()
{
}
}
So finally our third requirement is met, and we can run our tests.