我正在为服务编写单元测试,我需要模拟更复杂的 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中的代码示例:
var result = await _products
.Where(x => x.CategoryId == categoryId)
.Where(x => x.Stock > 0)
.OrderBy(x => x.Price)
.FirstOrDefaultAsync();
即使您假设
可以编写足够的起订量设置来模拟此交互,测试也会期望Where
完全使用这两个谓词进行调用。
var result = await _products
.Where(x => x.CategoryId == categoryId && x.Stock > 0)
.OrderBy(x => x.Price)
.FirstOrDefaultAsync();
这是一个完全等效的查询,应该会产生完全相同的答案,但它会破坏基于交互的测试。
或者,假设
Stock
是一个整数,有人可能会决定将查询更改为:
var result = await _products
.Where(x => x.CategoryId == categoryId)
.Where(x => x.Stock >= 1)
.OrderBy(x => x.Price)
.FirstOrDefaultAsync();
再次强调,这一分钟的重构
不应该改变系统的可观察行为,但它会破坏基于交互的测试。 最后,您可以将上述两个编辑结合起来:
var result = await _products
.Where(x => x.CategoryId == categoryId && x.Stock >= 1)
.OrderBy(x => x.Price)
.FirstOrDefaultAsync();
同样,这不会改变查询的行为,但会破坏基于交互的测试。
查询越复杂,编写它的方式就越有可能不止一种,并且更容易发生变化。
因此,我建议您
支持基于状态的测试而不是基于交互的测试。或者,正如 Jon Skeet 所写,使用 Fake Object 来代替。