Moq IDBContextFactory 与内存 EF Core

dem*_*ron 12 c# unit-testing moq entity-framework-core asp.net-core

我正在测试一个使用DbContext. 这个类得到一个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();
}
Run Code Online (Sandbox Code Playgroud)

我可以为一个测试进行设置,但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));
Run Code Online (Sandbox Code Playgroud)

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

System.ObjectDisposedException: Cannot access a disposed context instance.
Run Code Online (Sandbox Code Playgroud)

如果我再次致电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));
Run Code Online (Sandbox Code Playgroud)

这是Get(int id)方法:

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

据我了解,Mock 设置为返回

new SomeDbContext(new DbContextOptionsBuilder<SomeDbContext>()
                .UseInMemoryDatabase("InMemoryTest")
                .Options)
Run Code Online (Sandbox Code Playgroud)

每次都.CreateDbContext()被叫到。对我来说,这意味着它每次都应该返回一个新的上下文实例,而不是已经处理过的实例。但是,看起来它正在返回相同的已处置实例。

pok*_*oke 25

mockDbFactory.Setup(f => f.CreateDbContext())
    .Returns(new SomeDbContext(new DbContextOptionsBuilder<SomeDbContext>()
        .UseInMemoryDatabase("InMemoryTest")
        .Options));
Run Code Online (Sandbox Code Playgroud)

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

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

mockDbFactory.Setup(f => f.CreateDbContext())
    .Returns(() => new SomeDbContext(new DbContextOptionsBuilder<SomeDbContext>()
        .UseInMemoryDatabase("InMemoryTest")
        .Options));
Run Code Online (Sandbox Code Playgroud)

对于像您这样的简单事情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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

var repository = new Repository<SomeEntity>(new TestDbContextFactory());
Run Code Online (Sandbox Code Playgroud)


Ash*_*h K 6

除了@poke 的出色答案之外,这是我的完整答案,显示了添加测试数据。

步骤1:

在单元测试项目中安装这些 nuget 包:

  1. Microsoft.EntityFrameworkCore.InMemory
  2. 起订量
  3. 单位

第2步:

像这样设置测试:

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
    }
}
Run Code Online (Sandbox Code Playgroud)