Search code examples
c#unit-testingasp.net-coreentity-framework-coremoq

Moq IDBContextFactory with In-Memory EF Core


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.


Solution

  • 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());