Moq如何设置接收含指定Id的MyObject列表的方法?
我完全懂你踩的这个坑——Moq默认是按引用匹配对象的,你Setup时用的列表实例和实际调用时传入的根本不是同一个,自然匹配失败返回null。原方案直接用具体列表实例来设置确实不可行,除非测试代码和业务代码共用同一个列表对象,否则永远匹配不上。不过别担心,有几种灵活的办法能让你根据列表里MyObject的Id来精准匹配并返回对应结果。
方案1:用It.Is自定义匹配条件(最常用)
你可以通过It.Is<List<MyObject>>()传入lambda表达式,直接检查传入的列表是否符合你预期的Id规则,不管实例是不是同一个,只要Id对得上就行。
比如你想匹配同时包含fakeMyObject1.Id和fakeMyObject2.Id的列表:
// 假设你的服务方法叫ProcessObjects,返回类型为YourResultType mockService.Setup(s => s.ProcessObjects(It.Is<List<MyObject>>(list => // 检查列表里同时存在这两个Id的对象 list.Any(o => o.Id == fakeMyObject1.Id) && list.Any(o => o.Id == fakeMyObject2.Id) ))) .Returns(yourExpectedResult);
如果需要更严格的匹配(比如列表刚好只有这两个对象,不多不少),可以调整条件:
list.Count == 2 && list.All(o => o.Id == fakeMyObject1.Id || o.Id == fakeMyObject2.Id) && list.Any(o => o.Id == fakeMyObject1.Id) && list.Any(o => o.Id == fakeMyObject2.Id)
要是需要根据不同的Id组合返回不同结果,多写几个Setup即可:
// 当列表仅包含Id=1的对象时返回ResultA mockService.Setup(s => s.ProcessObjects(It.Is<List<MyObject>>(list => list.Count == 1 && list.First().Id == 1 ))) .Returns(ResultA); // 当列表同时包含Id=1和2时返回ResultB mockService.Setup(s => s.ProcessObjects(It.Is<List<MyObject>>(list => list.Any(o => o.Id == 1) && list.Any(o => o.Id == 2) ))) .Returns(ResultB);
方案2:重写MyObject的Equals和GetHashCode(需修改实体类)
如果你有权限修改MyObject类,可以重写它的Equals和GetHashCode方法,让Moq能按值(这里就是Id)比较对象。之后可以用SequenceEqual来匹配整个列表:
首先修改MyObject:
public class MyObject { public int Id { get; set; } public override bool Equals(object obj) { // 仅比较Id是否相等 return obj is MyObject other && Id == other.Id; } public override int GetHashCode() { return HashCode.Combine(Id); } }
然后Setup时可以这样写(要求列表元素的Id和顺序完全匹配):
mockService.Setup(s => s.ProcessObjects(It.Is<List<MyObject>>(list => list.SequenceEqual(new List<MyObject> { fakeMyObject1, fakeMyObject2 }) ))) .Returns(yourExpectedResult);
不过这个方案的局限性是必须修改实体类,且匹配时要求列表的元素顺序和数量完全一致,灵活性不如方案1。
关于原方案的说明
你最开始直接用new List<MyObject>() { fakeMyObject1, fakeMyObject2 }来Setup的方式不可行,因为Moq默认的匹配逻辑是检查传入参数是否和Setup时的参数是同一个引用实例。除非业务代码里刚好传入的就是这个列表对象(实际测试中几乎不可能),否则永远匹配不上,自然返回null。
内容的提问来源于stack exchange,提问作者Percy

