如何Mock TableClient.QueryAsync()实现按参数返回不同测试结果?
问题:Moq模拟TableClient.QueryAsync时无法按参数返回对应结果
待测试方法
public async Task<SomeClass> GetAsync(string partitionKey, string rowKey) { var entities = new List<SomeClass>(); await foreach (var e in _tableClient.QueryAsync<SomeClass>(x => x.PartitionKey == partitionKey && x.RowKey == rowKey)) { entities.Add(e); } return entities.FirstOrDefault(); }
尝试的Mock实现
var thingsToMock = new List<(string PartitionKey, string RowKey, string Value)>() { ("maxCount", "maxCount", "0"), ("maxCount", "xyz", "1000"), ("maxCount", "abc", "2000") }; var tableClientMock = new Mock<TableClient>(); foreach (var thingToMock in thingsToMock) { var returnPage = Page<SomeClass>.FromValues(new List<SomeClass> { new SomeClass{ PartitionKey = thingToMock.PartitionKey, RowKey = thingToMock.RowKey, Value = thingToMock.Value } }, null, new Mock<Response>().Object); var returnPages = AsyncPageable<SomeClass>.FromPages(new[] { returnPage }); Expression<Func<SomeClass, bool>> exp = (x) => x.PartitionKey == thingToMock.PartitionKey && x.RowKey == thingToMock.RowKey ? true : false; tableClientMock .Setup(i => i.QueryAsync<SomeClass>(It.Is<Expression<Func<SomeClass, bool>>>(expression => LambdaExpression.Equals(expression, exp)), null, null, default)) .Returns(returnPages); }
遇到的问题
- 调用
GetAsync("maxCount", "abc")时抛出Object reference not set to an instance of an object错误 - 改用
It.IsAny<Expression<Func<SomeClass, bool>>>()虽能运行,但无法根据不同的partitionKey和rowKey返回对应结果 - 使用的NuGet版本:
- Azure.Data.Tables 12.7.1
- Moq 4.14.1
解决方案
问题根源
LambdaExpression.Equals判断的是表达式实例的引用或结构完全一致性,但GetAsync中动态生成的表达式和Setup里定义的exp是不同实例,导致Mock匹配失败,QueryAsync返回null引发空引用异常。
实现步骤
- 编写自定义表达式匹配器:解析表达式中的
PartitionKey和RowKey比较值,与预期参数对比 - 修复循环闭包问题:捕获当前循环变量,避免所有Setup引用最后一个迭代项
- 重新配置Mock:使用自定义匹配器替代表达式实例相等判断
完整代码
自定义匹配方法
private static bool MatchesQueryExpression(Expression<Func<SomeClass, bool>> expression, string expectedPartitionKey, string expectedRowKey) { if (expression.Body is BinaryExpression andExpression && andExpression.NodeType == ExpressionType.AndAlso) { var leftCondition = andExpression.Left as BinaryExpression; var rightCondition = andExpression.Right as BinaryExpression; // 验证左侧是PartitionKey的相等判断 if (leftCondition?.Left is MemberExpression leftMember && leftMember.Member.Name == nameof(SomeClass.PartitionKey) && leftCondition.NodeType == ExpressionType.Equal && leftCondition.Right is ConstantExpression leftValue) { // 验证右侧是RowKey的相等判断 if (rightCondition?.Left is MemberExpression rightMember && rightMember.Member.Name == nameof(SomeClass.RowKey) && rightCondition.NodeType == ExpressionType.Equal && rightCondition.Right is ConstantExpression rightValue) { return leftValue.Value.ToString() == expectedPartitionKey && rightValue.Value.ToString() == expectedRowKey; } } } return false; }
修正后的Mock配置
var thingsToMock = new List<(string PartitionKey, string RowKey, string Value)>() { ("maxCount", "maxCount", "0"), ("maxCount", "xyz", "1000"), ("maxCount", "abc", "2000") }; var tableClientMock = new Mock<TableClient>(); foreach (var thingToMock in thingsToMock) { var currentMock = thingToMock; // 捕获当前循环变量,解决闭包问题 var returnPage = Page<SomeClass>.FromValues(new List<SomeClass> { new SomeClass{ PartitionKey = currentMock.PartitionKey, RowKey = currentMock.RowKey, Value = currentMock.Value } }, null, new Mock<Response>().Object); var returnPages = AsyncPageable<SomeClass>.FromPages(new[] { returnPage }); tableClientMock .Setup(i => i.QueryAsync<SomeClass>( It.Is<Expression<Func<SomeClass, bool>>>(expr => MatchesQueryExpression(expr, currentMock.PartitionKey, currentMock.RowKey)), null, null, default)) .Returns(returnPages); }
关键说明
- 自定义匹配器通过解析表达式的逻辑结构,提取实际传入的
PartitionKey和RowKey常量值,确保匹配逻辑正确 - 循环中使用
currentMock捕获变量,避免闭包导致所有Setup都绑定到最后一个循环项 - 该方案可以精准匹配不同参数组合的
QueryAsync调用,返回对应的模拟结果
内容的提问来源于stack exchange,提问作者stack247
相关产品推荐
相关产品推荐

