Kappacake
Kappacake

Reputation: 1947

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.

Upvotes: 14

Views: 9862

Answers (2)

Ash K
Ash K

Reputation: 3701

Adding to @poke's excellent answer, this is my complete answer that shows adding test data.

Step 1:

Install these nuget packages in your xUnit Test project:

  1. Microsoft.EntityFrameworkCore.InMemory
  2. Moq

Step 2:

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

poke
poke

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

Related Questions