如何模拟带表达式的泛型方法中Tuple与匿名类型的返回值?
首先可以明确说:是可以Mock的,但你当前的写法存在两个关键问题,导致Mock不会被正确命中,下面具体分析并给出解决方案:
问题1:匿名类型的类型隔离问题
你在Mock中创建的new { SelfRefObjectId = 1 },和业务代码里pr => new { pr.SelfRefObjectId }生成的匿名类型,本质上是两个不同的CLR类型——匿名类型的类型名称是编译器自动生成的,且仅在同一个编译上下文(比如同一个项目/程序集)内,结构完全一致的匿名类型才会被视为同一类型。而你的Mock代码和业务代码大概率不在同一个编译上下文,所以Moq无法匹配到这个Setup。
解决方案:
有两种常见处理方式:
改用具名类型替代匿名类型(推荐):
定义一个简单的DTO类来代替匿名类型,这样类型明确,Mock匹配毫无问题:public class EntityRefDto { public int SelfRefObjectId { get; set; } } // 业务代码里改为: var data = await _repository.GetByIdAsync(id, pr => new EntityRefDto { SelfRefObjectId = pr.SelfRefObjectId }); // Mock代码: _repositoryMock .Setup(pr => pr.GetByIdAsync(entityId, It.IsAny<Expression<Func<Entity, EntityRefDto>>>(), default)) .ReturnsAsync(new EntityRefDto { SelfRefObjectId = 1 }) .Verifiable();使用宽松的表达式匹配:
如果一定要保留匿名类型,可以用It.IsAny<Expression<Func<Entity, object>>>()来匹配任意表达式,然后返回对应的匿名类型实例:_repositoryMock .Setup(r => r.GetByIdAsync<object>(entityId, It.IsAny<Expression<Func<Entity, object>>>(), default)) .ReturnsAsync(new { SelfRefObjectId = 1 }) .Verifiable();注意:这种方式会匹配所有返回
object类型的GetByIdAsync调用,如果你的测试里有多个不同的匿名类型调用,需要额外区分。
问题2:Tuple的表达式树匹配逻辑
Moq默认是按引用比较表达式树的——你在Setup里写的pr => Tuple.Create(pr, pr.SelfRefObject)和业务代码里的同一个表达式,虽然结构完全一致,但它们是两个不同的表达式树实例,所以Moq不会认为这两个表达式相等,导致Setup不命中。
解决方案:
同样有两种方式:
宽松匹配(简单直接):
用It.IsAny<Expression<...>>()匹配任意返回Tuple的表达式:var tupleReturnValue = Tuple.Create(new Entity(), new Entity()); _repositoryMock .Setup(r => r.GetByIdAsync<Tuple<Entity, Entity>>(entityId, It.IsAny<Expression<Func<Entity, Tuple<Entity, Entity>>>>(), default)) .ReturnsAsync(tupleReturnValue) .Verifiable();自定义表达式相等比较(精确匹配):
如果你需要精确匹配表达式的结构,可以实现一个表达式树相等比较器,然后在Setup中用It.Is<Expression<...>>()来校验:// 简单的表达式树相等比较器实现(仅针对你的Tuple场景) public class ExpressionEqualityComparer : IEqualityComparer<Expression> { public bool Equals(Expression x, Expression y) { if (x == null && y == null) return true; if (x == null || y == null) return false; // 这里可以扩展更全面的表达式比较逻辑,下面仅针对你的Tuple场景 var xLambda = x as LambdaExpression; var yLambda = y as LambdaExpression; if (xLambda == null || yLambda == null) return false; // 检查是否是调用Tuple.Create的表达式 var xCall = xLambda.Body as MethodCallExpression; var yCall = yLambda.Body as MethodCallExpression; if (xCall == null || yCall == null) return false; return xCall.Method == yCall.Method && Equals(xCall.Arguments[0], yCall.Arguments[0]) && Equals(xCall.Arguments[1], yCall.Arguments[1]); } public int GetHashCode(Expression obj) => obj.GetHashCode(); } // 使用方式: var tupleReturnValue = Tuple.Create(new Entity(), new Entity()); _repositoryMock .Setup(r => r.GetByIdAsync(entityId, It.Is<Expression<Func<Entity, Tuple<Entity, Entity>>>>(e => new ExpressionEqualityComparer().Equals(e, pr => Tuple.Create(pr, pr.SelfRefObject))), default)) .ReturnsAsync(tupleReturnValue) .Verifiable();这种方式可以精确匹配表达式结构,但实现比较繁琐,如果不是必须的话,宽松匹配更省心。
总结
- 匿名类型的Mock难点在于类型隔离,推荐改用具名类型;
- Tuple的Mock难点在于表达式树的引用比较,宽松匹配是最便捷的方案;
- 如果需要精确匹配,都可以通过自定义表达式比较逻辑实现,但成本较高。
内容的提问来源于stack exchange,提问作者Józef Podlecki

