我正在尝试模拟我的 dbContext 以测试我的存储库查询/过滤器。我现在已经尝试使用内存数据库或模拟数据库上下文。在这两种情况下,我只能在不使用异步调用时才能使其工作。另外,对于 MockQueryable.NSubstitute,在运行 FirstOrDefaultAsync 之前有额外的过滤条件时,我无法让它工作。
我总是例外:“IQueryable”没有实现“IAsyncQueryProvider”
我不知道在哪里以及如何实施该提供程序。还有好的例子吗?
测试设置:
[Fact]
public async Task Get_ById_Returns_CorrectRequirement()
{
// Arrange
long requirementId = 7;
string ntUser = "anyUser";
var options = new DbContextOptionsBuilder<MyDbContext>()
.UseInMemoryDatabase(databaseName: "TestDatabase")
.Options;
using var context = new MyDbContext(options);
context.TDummyTable.AddRange(new List<TDummyTable>
{
new TDummyTable { PkKey = 1, Valid = true },
new TDummyTable { PkKey = requirementId, Valid = true },
});
await context.SaveChangesAsync();
// Act
var requirement = await _sut.Get(requirementId, ntUser);
// Assert
Assert.Equal(requirementId, requirement.PkKey);
}
回购方法:
public async Task<TDummyTable> Get(long id, string userNt)
{
var itemsQuery = this.context.TDummyTable.AsQueryable();
itemsQuery = itemsQuery.Where(x => x.PkKey == 23);
return await itemsQuery.FirstOrDefaultAsync(x => x.PkKey.Equals(id));
}
编辑: 这就是我尝试在排列中使用 MockQueryable 的方式
var data = new List<TDummyTable>
{
new TDummyTable { PkKey = 1, Valid = true},
new TDummyTable { PkKey = requirementId, Valid = true},
};
var mock = data.AsQueryable().BuildMockDbSet();
context.TDummyTable.Returns(mock);
不幸的是,我没有 NSubstitute 的经验,因为我使用 Moq 进行模拟,但如果您使用存储库模式,那么想法通常是让存储库作为模拟的边界,避免需要深入到
DbContext
/DbSet
。
存储库是域数据的看门人。业务逻辑存在于控制器、服务等中,这就是您想要测试的内容。因此,当您的业务逻辑将转到存储库的 .Get() 方法时,您可以模拟存储库,而不是使用模拟的 DbContext 调用具体存储库
var mockRepo = new Mock<DummyTableRepository>();
mockRepo.Setup(x => x.GetAsync(expectedId, expectedUserNt))
.ReturnsAsync( new TDummyTable { PkKey = expectedId, Valid = true });
上面是 Moq,但是 NSubstitute 应该有类似的功能来处理模拟异步调用。
当前方法的问题是您正在尝试测试 Linq,
Where
条件可以从集合中过滤行。 Linq 已经测试过,EF 也已经测试过。现在,您可能在存储库方法中强制执行一些更复杂的条件,但我建议将这些条件断言为集成测试(针对实时数据库的测试)的一部分,这些测试作为持续集成的一部分运行,而不是写下单元测试在数据库级别,甚至使用内存数据库之类的东西。原因是内存中的 Linq 和给定数据库的 SQL 之间的行为可能会发生变化,而且大多数情况下单元测试应该关注业务逻辑,即本质上可变的东西。
MockQueryable 发挥作用的地方:
有时您的存储库不会返回单个实体,而是返回实体集。一种选择是简单地让存储库返回
IEnumerable<TEntity>
或 IList<TEntity>
,但这不是很有效。您可能希望使用投影来获取一组 ViewModel、分页、应用排序等。此外还可以公开同步和异步风格。将所有这些问题传递到存储库非常复杂,并且会导致大量的样板文件。所以一种选择是允许存储库返回IQueryable<TEntity>
。通过这种方式,存储库的调用者可以准确决定如何使用结果。模拟存储库不能简单地传回 new [] { new TEntity {...}, new TEntity {...}, ... }.AsQueryable();
if 消费者可能正在对其使用异步调用,因此您可以使用 MockQueryable 来返回:
var query = new []
{
new TEntity {...},
new TEntity {...},
...
}.BuildMock();
这样,存储库的使用者就可以对结果使用同步或异步调用。
MockQueryable 有一个
BuildMockDbSet()
,适用于被测试的服务使用注入的 DbContext
而不是存储库的情况。您将创建 DbContext
的模拟,然后初始化它对 DbSet
实例的 BuildMockDbSet()
引用。看起来这可能是您在注入存储库的 DbContext 中尝试的操作,但看起来这个示例并不是完整的图片,可以看出为什么它不起作用。由于您有一个存储库,我强烈建议避免这种复杂性,而是使用存储库作为单元测试的边界。