Reputation: 1947
I am testing a class that uses a DbContext
. This class gets an IDbContextFactory
injected, which is then used to get a DbContext
:
protected readonly IDbContextFactory<SomeDbContext> ContextFactory;
public Repository(IDbContextFactory<SomeDbContext> contextFactory)
{
ContextFactory = contextFactory;
}
public List<T> Get()
{
using var db = ContextFactory.CreateDbContext();
return db.Set<T>().ToList();
}
I am able to set things up for ONE test, but I have to call the Mock<DbContextFactory>.Setup(f => f.CreateDbContext())
method every time I want to use the context.
Here is an example:
var mockDbFactory = new Mock<IDbContextFactory<SomeDbContext>>();
mockDbFactory.Setup(f => f.CreateDbContext())
.Returns(new SomeDbContext(new DbContextOptionsBuilder<SomeDbContext>()
.UseInMemoryDatabase("InMemoryTest")
.Options));
var repository = new Repository<SomeEntity>(mockDbFactory.Object);
// non-existent id
Assert.IsNull(repository.Get(-1));
This works fine. However, if I add another repo call (like Assert.DoesNotThrow(() => repository.Get(1);
), I get
System.ObjectDisposedException: Cannot access a disposed context instance.
If I call the Mock<T>.Setup()
again, all works fine
var mockDbFactory = new Mock<IDbContextFactory<SomeDbContext>>();
mockDbFactory.Setup(f => f.CreateDbContext())
.Returns(new SomeDbContext(new DbContextOptionsBuilder<SomeDbContext>()
.UseInMemoryDatabase("InMemoryTest")
.Options));
var repository = new Repository<SomeEntity>(mockDbFactory.Object);
// non-existent id
Assert.IsNull(repository.Get(-1));
mockDbFactory.Setup(f => f.CreateDbContext())
.Returns(new SomeDbContext(new DbContextOptionsBuilder<SomeDbContext>()
.UseInMemoryDatabase("InMemoryTest")
.Options));
// pass
Assert.DoesNotThrow(() => repository.Get(1));
This is the Get(int id)
method:
public T Get(int id)
{
using var db = ContextFactory.CreateDbContext();
return db.Set<T>().Find(id);
}
As far as I understand, Mock is setup to return
new SomeDbContext(new DbContextOptionsBuilder<SomeDbContext>()
.UseInMemoryDatabase("InMemoryTest")
.Options)
Every time .CreateDbContext()
is called. To me, this means that it should return a new instance of the context every time, rather than the one that has already been disposed. However, it looks like it is returning the same disposed instance.
Upvotes: 14
Views: 9862
Reputation: 3701
Adding to @poke's excellent answer, this is my complete answer that shows adding test data.
Install these nuget packages in your xUnit Test project:
Setup the test like so:
using Moq;
using Microsoft.EntityFrameworkCore;
namespace SomeProject.UnitTests;
public class SomeTests
{
[Fact]
public async Task Some_Test_Should_Do_Something()
{
// ARRANGE
var mockDbFactory = new Mock<IDbContextFactory<SomeDbContext>>();
var options = new DbContextOptionsBuilder<SomeDbContext>()
.UseInMemoryDatabase(databaseName: "SomeDatabaseInMemory")
.Options;
// Insert seed data into the database using an instance of the context
using (var context = new SomeDbContext(options))
{
context.SomeEntities.Add(new SomeEntity { Id = 1, SomeProp = "Some Prop Value" });
context.SomeEntities.Add(new SomeEntity { Id = 2, SomeProp = "Some Prop Value" });
context.SaveChanges();
}
// Now the in-memory db already has data, we don't have to seed everytime the factory returns the new DbContext:
mockDbFactory.Setup(f => f.CreateDbContextAsync(It.IsAny<CancellationToken>())).ReturnsAsync(() => new SomeDbContext(options));
// ACT
var serviceThatNeedsDbContextFactory = new SomeService(mockDbFactory.Object);
var result = await serviceThatNeedsDbContextFactory.MethodThatIsBeingTestedAsync();
// ASSERT
// Assert the result
}
}
Upvotes: 9
Reputation: 388273
mockDbFactory.Setup(f => f.CreateDbContext())
.Returns(new SomeDbContext(new DbContextOptionsBuilder<SomeDbContext>()
.UseInMemoryDatabase("InMemoryTest")
.Options));
This sets up your mock with a single instance. This instance will be returned every time your CreateDbContext
method is called on the mock. Since your methods (correctly) dispose the database context after each use, the first call will dispose this shared context which means that every later call to CreateDbContext
returns the already-disposed instance.
You can change this by passing a factory method to Returns
instead that creates a new database context each time:
mockDbFactory.Setup(f => f.CreateDbContext())
.Returns(() => new SomeDbContext(new DbContextOptionsBuilder<SomeDbContext>()
.UseInMemoryDatabase("InMemoryTest")
.Options));
For simple things like your IDbContextFactory<>
, assuming that it only has this single CreateDbContext
method, you could also just create a real test implementation instead of creating the mock every time:
public class TestDbContextFactory : IDbContextFactory<SomeDbContext>
{
private DbContextOptions<SomeDbContext> _options;
public TestDbContextFactory(string databaseName = "InMemoryTest")
{
_options = new DbContextOptionsBuilder<SomeDbContext>()
.UseInMemoryDatabase(databaseName)
.Options;
}
public SomeDbContext CreateDbContext()
{
return new SomeDbContext(_options);
}
}
Then you could just use that directly in your tests which might be a bit more readable than having to deal with a mock in this case:
var repository = new Repository<SomeEntity>(new TestDbContextFactory());
Upvotes: 29