Moq IDBContextFactory 与 In-Memory EF Core

Moq IDBContextFactory with In-Memory EF Core

我正在测试使用 DbContext 的 class。这个 class 得到一个 IDbContextFactory 注入,然后用来得到一个 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();
}

我可以为一项测试进行设置,但每次我想使用上下文时都必须调用 Mock<DbContextFactory>.Setup(f => f.CreateDbContext()) 方法。

这是一个例子:

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

这很好用。但是,如果我添加另一个回购调用(如 Assert.DoesNotThrow(() => repository.Get(1);),我会得到

System.ObjectDisposedException: Cannot access a disposed context instance.

如果我再次调用 Mock<T>.Setup(),一切正常

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

这是Get(int id)方法:

public T Get(int id)
{
    using var db = ContextFactory.CreateDbContext();
    return db.Set<T>().Find(id);
}

据我了解,Mock 设置为 return

new SomeDbContext(new DbContextOptionsBuilder<SomeDbContext>()
                .UseInMemoryDatabase("InMemoryTest")
                .Options)

每次调用.CreateDbContext()。对我来说,这意味着它每次都应该 return 上下文的一个新实例,而不是已经处理掉的实例。但是,看起来它正在 return 处理同一个实例。

mockDbFactory.Setup(f => f.CreateDbContext())
    .Returns(new SomeDbContext(new DbContextOptionsBuilder<SomeDbContext>()
        .UseInMemoryDatabase("InMemoryTest")
        .Options));

这将使用 单个实例 设置您的模拟。每次在模拟上调用 CreateDbContext 方法时都会返回此实例。由于您的方法(正确地)在每次使用后处理数据库上下文,因此第一次调用将处理此共享上下文,这意味着以后每次调用 CreateDbContext returns 已经处理的实例。

您可以通过将工厂方法传递给 Returns 来更改此设置,而不是每次都创建一个新的数据库上下文:

mockDbFactory.Setup(f => f.CreateDbContext())
    .Returns(() => new SomeDbContext(new DbContextOptionsBuilder<SomeDbContext>()
        .UseInMemoryDatabase("InMemoryTest")
        .Options));

对于像您的 IDbContextFactory<> 这样简单的事情,假设它只有一个 CreateDbContext 方法,您也可以只创建一个真正的测试实现而不是每次都创建模拟:

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);
    }
}

然后你可以直接在你的测试中使用它,这可能比在这种情况下必须处理模拟更具可读性:

var repository = new Repository<SomeEntity>(new TestDbContextFactory());