模拟Entity Framework Core上下文

11

我试图测试我的应用程序,所以我需要模拟我的EF上下文。

我的代码似乎没问题,但是我遇到以下异常:

"System.ArgumentNullException:无法将空值分配给对象。参数名:source"

这是我的测试方法:

  var options = new DbContextOptionsBuilder<ProductContext>().Options;
    var settings = new SqlSettings
    {
        InMemory = true
    };

    var context = new Mock<ProductContext>(options, settings);
    var mockTreeService = new TreeService(context.Object);
    await mockTreeService.CreateTreeAsync("Testing tree", Guid.NewGuid());

    context.Verify(x => x.AddAsync(It.IsAny<Tree>(), CancellationToken.None), Times.Once);

看起来在执行这段代码时抛出了这个异常

            var tree = await _context.Trees
                .Include(x => x.Translation)
                .FirstOrDefaultAsync(x => x.Translation.Pl == name);

这来自于我正在测试的服务


在我看来,“include”会带来问题,但我不知道该如何解决。 - bielu000
1
你可能会发现EF Core Testing文档很有用。 - bricelam
你不需要在EF Core中模拟ProductContext,而是使用InMemory ProductContext。 - Lapenkov Vladimir
4个回答

37

我认为这是因为没有设置连接字符串。老实说,要完全模拟DbContext有点困难,这就是为什么EF Core团队提供了一种内存实现的原因。这样对于测试来说更容易使用。只需将您的options初始化更改为:

var options = new DbContextOptionsBuilder<ProductContext>()
                  .UseInMemoryDatabase(Guid.NewGuid().ToString())
                  .Options;

接下来,您需要使用测试数据填充数据库。然后,您可以运行其余的测试。

注意:如果您正在使用内存数据库,则不再需要模拟上下文,因此可以删除那部分代码。内存数据库本身就是一个模拟。


1
为什么需要使用模拟?您可以设置数据库的起始状态,执行某些操作并在之后检查状态。如果状态在操作后符合预期,则测试通过。它调用上下文中的一个或另一个方法是实现细节,从测试角度来看并不重要。您测试结果而不是实现。 - Chris Pratt
例如,如何使用Moq中的Verify方法。 - bielu000
1
再说,这只是实现细节。无论如何,在这种情况下你都不应该测试它。如果底层逻辑发生变化,你的测试将会失败,即使结果仍然相同。那就是一个糟糕的测试。 - Chris Pratt
3
我必须在我的测试项目中引用 Microsoft.EntityFrameworkCore.InMemory! - Carl Verret
1
微软建议您不要使用内存数据库进行测试,而是使用仓储模式来封装上下文。https://docs.microsoft.com/zh-cn/ef/core/testing/choosing-a-testing-strategy#inmemory-as-a-database-fake 我不确定我完全同意微软的观点。我已经在 IDbContext 上使用了内存数据库和 MOQ,它们本质上是相同的,它们都运行良好。封装在上下文周围的仓储确实有助于抽象 EF Core,但它是多余的并且维护成本高昂。我的看法是:对于简单的数据库访问测试,请使用内存数据库。 - Blake
显示剩余4条评论

6

我使用了这个https://github.com/huysentruitw/entity-framework-core-mock库。非常容易,可以用更少的代码编写单元测试。

如果您正在使用moq框架,您可以使用大多数Moq方法。

以下是测试DBQuerys的示例代码。

public async Task<Boat> GetByIdAsync(string id)
    => await _boatContext.Boats.Where(x => x.id == id).FirstOrDefaultAsync();

[Fact]
public async Task GetByIdAsync_WhenCalled_ReturnsItem()
{
    // Arrange
    var models = new[] { new Boat { id = "p1" } };
    var dbContextMock = new DbContextMock<BoatContext>();
    dbContextMock.CreateDbQueryMock(x => x.Boats, models);

    var service = new Properties(dbContextMock.Object);

    // Act
    var okResult = await service.GetByIdAsync("p1");

    // Assert
    Assert.IsType<Boat>(okResult.Result);
}

在这里发布可能会帮助某些人 :)


1
我认为对DbContext进行模拟是不正确的。在测试中,您应该模拟repositories... 模拟DbContext实际上是在测试Microsoft的代码...这很愚蠢,因为他们已经这样做了。所以...所有数据访问都应通过repositories(请参见Repository Pattern)进行,并且您应该在测试中模拟它们,而不是DbContext

1
通常来说,测试现成的代码,特别是来自像Microsoft这样的分销商的代码是浪费的。但在实体框架的情况下,有巨大的好处,因为您可以获得测试配置的能力。 - Louis
1
我并不是说你不应该这样做。我是说,当正确地进行 dbcontext 模拟时,可以帮助你测试“连接”、“关系”和其他配置。这非常有用。 - Louis
1
我的天啊...我只是在说...这是一个好主意-https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/in-memory。如果你还不同意也没关系,这只是我的观点。我从未说过或暗示过你要在存储库之外测试任何东西;我建议你通过模拟你的DbContext来测试你的存储库。使用代码优先方式模拟你的DbContext对我很有效。不同意吗?抱歉,我无法帮助你改变想法。这只是我的个人观点。顺便说一下,当我说Repo时,我并不是指你的DbContext类,而是指直接使用你的上下文的消费者。它在你的存储和“领域”之间进行转换。 - Louis
9
仓储模式的关键是处理领域层的持久化。EF已经能够胜任这个任务。将仓储套在EF外面是多余和无用的。选择EF或其他ORM是选择使用第三方DAL而不是编写自己的。如果您打算自己编写,那么请放弃EF,直接使用SQL更好。这样至少还有一些意义,您的应用程序可能会更容易维护并且效率更高。 - Chris Pratt
2
我不想重新点燃以前的争论,但我确实看到了模拟上下文的价值。当上下文以某种方式行为时,您希望存储库以某种方式行为。确保存储库正确“反应”上下文的一种方法是模拟上下文的操作方式。您可以设置上下文来执行任何操作,并确保在该场景中存储库按照您的要求执行操作。 - devklick
显示剩余8条评论

1

尝试使用我的Moq/NSubstitute扩展MockQueryable: https://github.com/romantitov/MockQueryable,支持所有同步/异步操作。

//1 - create a List<T> with test items
var users = new List<UserEntity>()
{
 new UserEntity,
 ...
};

//2 - build mock by extension
var mock = users.AsQueryable().BuildMock();

//3 - setup the mock as Queryable for Moq
_userRepository.Setup(x => x.GetQueryable()).Returns(mock.Object);

//3 - setup the mock as Queryable for NSubstitute
_userRepository.GetQueryable().Returns(mock);

DbSet 也受支持。

//2 - build mock by extension
var mock = users.AsQueryable().BuildMockDbSet();

//3 - setup DbSet for Moq
var userRepository = new TestDbSetRepository(mock.Object);

//3 - setup DbSet for NSubstitute
var userRepository = new TestDbSetRepository(mock);

注意:

  • AutoMapper 支持从 1.0.4 版本开始
  • DbQuery 支持从 1.1.0 版本开始

我尝试了你的NuGet包,但是我没有看到任何更复杂设置的方法。我需要测试一个带有连接查询的查询语句,但我不知道如何告诉你的包涉及到多个模拟DbSet。(只是来这里寻找你的包的替代方案,很高兴在这里见到你。) - Klom Dark
@KlomDark谢谢你的反馈。看到你无法使用MockQueryable测试代码让我很难过。我正在努力改进这个项目。如果你发现MockQueryable缺少你需要的功能,你可以在GitHub页面上创建一个详细描述的问题。欢迎你通过Pull Request贡献新功能。 - R.Titov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接