NUnit中List<T>的.Is.EquivalentTo断言不符合预期问题排查
问题:自定义对象集合的NUnit等价断言失败,字符串列表集合正常
现象说明
在.NET 4.5.2项目中使用NUnit框架的Assert.That(first, Is.EquivalentTo(second))进行集合无序等价断言时:
List<List<string>>类型的两个集合测试通过List<SimpleObject>类型的属性值完全一致的集合断言失败
通过的测试代码
[Test] public void Test_Int_ExampleDeepListCompare() { List<List<string>> first = new List<List<string>>() { new List<string> { "1", "3" }, new List<string> { "1", "2" }, new List<string> { "a", "b" } }; List<List<string>> second = new List<List<string>>() { new List<string> { "1", "2" }, new List<string> { "1", "3" }, new List<string> { "a", "b" } }; Assert.That(first, Is.EquivalentTo(second)); }
失败的测试代码
[Test] public void Test_Int_ExampleDeepCompareCustomObjects2() { List<SimpleObject> rtnValFakeA = new List<SimpleObject>() { new SimpleObject() { FirstName = "Bob", LastName = "Jones", Mi = "a", StudId = 12345 } }; List<SimpleObject> rtnValFakeb = new List<SimpleObject>() { new SimpleObject() { FirstName = "Bob", LastName = "Jones", Mi = "a", StudId = 12345 } }; //assert with deep compare ignoring order - EWB Assert.That(rtnValFakeA, Is.EquivalentTo(rtnValFakeb)); }
SimpleObject定义
public class SimpleObject { public string LastName { get; set; } public string FirstName { get; set; } public string Mi { get; set; } public Int64 StudId { get; set; } }
失败提示
Expected: equivalent to < <_Test_DAL.SimpleObject> >
But was: < <_Test_DAL.SimpleObject> >
原因分析
- 内置类型的相等性实现:
List<string>是.NET内置集合,它重写了Equals和GetHashCode方法,会深度比较集合内的所有元素是否相等,因此NUnit能正确识别两个List<string>实例的等价性。 - 自定义类型的默认相等性:自定义的
SimpleObject类默认使用引用相等(即比较对象的内存地址),即使两个实例的属性值完全相同,它们也是不同的对象实例,NUnit会判定这两个对象不等价,最终导致集合断言失败。 Is.EquivalentTo的工作逻辑:该断言用于判断两个集合是否包含相同的元素(不考虑顺序),但元素的相等性完全依赖于元素类型自身的Equals方法实现。
解决方案(适配.NET 4.5.2)
方案1:重写SimpleObject的Equals和GetHashCode方法
直接修改自定义类,让它基于属性值判断相等性:
public class SimpleObject { public string LastName { get; set; } public string FirstName { get; set; } public string Mi { get; set; } public Int64 StudId { get; set; } public override bool Equals(object obj) { // 先判断类型是否匹配 if (obj is not SimpleObject other) return false; // 比较所有属性值 return LastName == other.LastName && FirstName == other.FirstName && Mi == other.Mi && StudId == other.StudId; } public override int GetHashCode() { // 计算哈希值,确保属性相同的对象哈希值一致 unchecked { int hash = 17; hash = hash * 23 + (LastName?.GetHashCode() ?? 0); hash = hash * 23 + (FirstName?.GetHashCode() ?? 0); hash = hash * 23 + (Mi?.GetHashCode() ?? 0); hash = hash * 23 + StudId.GetHashCode(); return hash; } } }
修改后,原有的断言代码无需改动即可正常通过。
方案2:使用自定义相等比较器(不修改原类)
如果无法修改SimpleObject类,可以定义一个实现IEqualityComparer<SimpleObject>的比较器,在断言中指定使用该比较器:
定义比较器
public class SimpleObjectEqualityComparer : IEqualityComparer<SimpleObject> { public bool Equals(SimpleObject x, SimpleObject y) { if (x == null && y == null) return true; if (x == null || y == null) return false; return x.LastName == y.LastName && x.FirstName == y.FirstName && x.Mi == y.Mi && x.StudId == y.StudId; } public int GetHashCode(SimpleObject obj) { if (obj == null) return 0; unchecked { int hash = 17; hash = hash * 23 + (obj.LastName?.GetHashCode() ?? 0); hash = hash * 23 + (obj.FirstName?.GetHashCode() ?? 0); hash = hash * 23 + (obj.Mi?.GetHashCode() ?? 0); hash = hash * 23 + obj.StudId.GetHashCode(); return hash; } } }
修改断言代码
Assert.That(rtnValFakeA, Is.EquivalentTo(rtnValFakeb).Using(new SimpleObjectEqualityComparer()));
方案3:使用属性匹配断言(临时场景)
如果只是单次测试需要验证,也可以直接通过属性匹配的方式逐个验证元素,无需修改类或定义比较器:
// 验证集合数量一致 Assert.That(rtnValFakeA.Count, Is.EqualTo(rtnValFakeb.Count)); // 验证每个元素都能在另一个集合中找到属性匹配的对象 Assert.That(rtnValFakeA, Has.All.Matches<SimpleObject>(item => rtnValFakeb.Any(other => item.FirstName == other.FirstName && item.LastName == other.LastName && item.Mi == other.Mi && item.StudId == other.StudId)));
这种方式适合简单场景,但集合元素较多时代码会比较繁琐。
内容的提问来源于stack exchange,提问作者Eric Brown - Cal
相关产品推荐
相关产品推荐

