我正在为一项服务编写单元测试,我需要模拟一个更复杂的 LINQ 查询,其中包括条件逻辑和异步方法,例如FirstOrDefaultAsync()
。
查询涉及几种链式方法,如过滤、排序和选择:
var result = await _products
.Where(x => x.CategoryId == categoryId)
.Where(x => x.Stock > 0)
.OrderBy(x => x.Price)
.FirstOrDefaultAsync();
我想模拟整个查询的行为,包括Where()
、OrderBy()
和FirstOrDefaultAsync()
方法,并提供自定义结果以测试不同的场景。
我正在使用 Moq 来模拟数据层,但我不确定如何模拟整个查询链并使其异步。
正如Jon Skeet 所言,Moq 并不适合这种测试。我无法确切地说出所提到的准备阶段到底发生了什么(或者,我不知道如何看待该讨论),所以我将尝试详细说明。
Where
诸如、OrderBy
和 之类的函数FirstOrDefaultAsync
是扩展方法,因此不能通过Test Double进行替换。Stack Overflow 上有很多关于使用 Moq 测试扩展方法的问题,答案总是:不能。下面是其中一个:如何使用 Moq 模拟扩展方法?您可能想弄清楚如何
Where
在IEnumerable<T>
或之上实现扩展方法IQueryable<T>
,但这会使您的测试变得复杂而脆弱。测试将测试Where
、OrderBy
等的实现细节。此外,如果不访问每个扩展方法的源代码,这将很难做到,而且我不确定这些扩展FirstOrDefaultAsync
方法是否是开源的。此外,如果您使用基于交互的测试,那么您将面临一项脆弱测试。该测试对于预期交互的构成要素的理解过于狭隘。
考虑来自 OP 的代码示例:
即使您假设您可以编写足够的 Moq 设置来模拟这种交互,测试也会期望
Where
使用这两个谓词精确地调用它。如果另一个程序员过来并决定将这两个谓词融合在一起(这将是适当的优化)会发生什么?
这是一个完全等效的查询,应该会得出完全相同的答案,但它会破坏基于交互的测试。
或者,假设它
Stock
是一个整数,有人可能会决定将查询更改为:再次强调,这种微小的重构不应该改变系统的可观察行为,但它会破坏基于交互的测试。
最后,您可以将以上两项编辑合并为以下内容:
同样,这不会改变查询的行为,但会破坏基于交互的测试。
查询越复杂,编写方式就越有可能不止一种,而且越容易发生变化。
因此,我建议您优先使用基于状态的测试,而不是基于交互的测试。或者,如 Jon Skeet 所写,改用Fake Object。