【问题标题】:Moq IDBContextFactory with In-Memory EF CoreMoq IDBContextFactory 与 In-Memory EF Core
【发布时间】:2021-02-08 12:26:42
【问题描述】:

我正在测试一个使用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();
}

我可以为 ONE 测试进行设置,但每次我想使用上下文时都必须调用 Mock&lt;DbContextFactory&gt;.Setup(f =&gt; 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));

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

System.ObjectDisposedException: Cannot access a disposed context instance.

如果我再次拨打Mock&lt;T&gt;.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 设置为返回

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

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

【问题讨论】:

    标签: c# unit-testing asp.net-core entity-framework-core moq


    【解决方案1】:
    mockDbFactory.Setup(f => f.CreateDbContext())
        .Returns(new SomeDbContext(new DbContextOptionsBuilder<SomeDbContext>()
            .UseInMemoryDatabase("InMemoryTest")
            .Options));
    

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

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

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

    对于像你的IDbContextFactory&lt;&gt; 这样简单的事情,假设它只有一个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());
    

    【讨论】:

    • 在这种情况下如何设置测试数据?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-11-03
    • 2015-12-23
    • 1970-01-01
    • 2017-04-26
    • 1970-01-01
    相关资源
    最近更新 更多