如何正确使用C#自动化测试中的AreEqual方法实现自定义类相等校验
C# 大型自定义类相等校验的优雅实现方案
以下是三种适配不同场景的解决方案,可替代手动逐字段编写Equals的实现:
方案1:使用record类型(C# 9+ 版本支持)
- 适用场景:项目支持C# 9及以上版本,且
MainMemento为纯数据承载类(备忘录模式的存储类天然符合该特征) - 实现优势:编译器会自动为record类型生成值相等逻辑,递归比较所有属性的取值,新增属性时不需要手动修改比较逻辑,代码量最少
- 实现方式:仅需将类声明修改为record,删除自定义的
Equals和GetHashCode方法即可,示例:
[Serializable] public record MainMemento { public StateMemento StateMap { get; set; } public DepartmentEffectsMemento DepartmentEffects { get; set; } public GeneralDatabaseMemento<DocumentInstanceID, DocumentInstance> DocumentDB { get; set; } public GeneralDatabaseMemento<BuildingID, Building> BuildingDB { get; set; } public GeneralDatabaseMemento<CityActionID, CityAction> CityActionDB { get; set; } public int PlayerWeekToSkip { get; set; } public DocumentsHandlerMemento DocumentsHandler { get; set; } public List<KeyValuePair<GameLaws.Law, LawStatus>> Laws { get; set; } public Boolean SimulationState { get; set; } public Session.Settings Settings { get; set; } public ElementsPoolMemento<SaveableSprite> portraitPool { get; set; } }
测试代码不需要做任何修改,原有Assert.AreEqual可正常执行。
方案2:通用反射深度比较工具
- 适用场景:无法使用高版本C#的项目,且生产代码中需要频繁比较该类实例的相等性
- 实现优势:一次实现可复用给所有数据类,不需要为每个类单独编写逐字段比较逻辑,支持嵌套类型、集合类型的递归比较
- 实现示例:
using System.Reflection; using System.Collections; public static class DeepComparison { public static bool DeepEquals<T>(T a, T b) { // 引用相等直接返回true if (ReferenceEquals(a, b)) return true; // 空值判断 if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) return false; Type type = typeof(T); // 值类型、字符串直接比较 if (type.IsValueType || type == typeof(string)) return a.Equals(b); // 集合类型逐元素比较 if (typeof(IEnumerable).IsAssignableFrom(type)) { var enumA = ((IEnumerable)a).Cast<object>(); var enumB = ((IEnumerable)b).Cast<object>(); // 先判断长度是否相等 if (enumA.Count() != enumB.Count()) return false; return enumA.Zip(enumB, DeepEquals).All(x => x); } // 引用类型遍历所有公开实例属性比较 foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { var valA = prop.GetValue(a); var valB = prop.GetValue(b); if (!DeepEquals(valA, valB)) return false; } return true; } // 配套的GetHashCode生成方法,若需要用哈希容器可调用 public static int GetDeepHashCode<T>(T obj) { if (ReferenceEquals(obj, null)) return 0; Type type = typeof(T); if (type.IsValueType || type == typeof(string)) return obj.GetHashCode(); var hashCode = new HashCode(); if (typeof(IEnumerable).IsAssignableFrom(type)) { foreach (var item in (IEnumerable)obj) { hashCode.Add(GetDeepHashCode(item)); } return hashCode.ToHashCode(); } foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { hashCode.Add(GetDeepHashCode(prop.GetValue(obj))); } return hashCode.ToHashCode(); } }
使用时可以直接在测试中调用Assert.IsTrue(DeepComparison.DeepEquals(data.Item1, data.Item2)),也可以将自定义Equals方法替换为调用该工具类,避免逐字段编码。
方案3:测试侧结构化断言
- 适用场景:仅测试场景需要比较相等性,生产代码不需要重写类的Equals逻辑,无需修改现有业务类代码
- 实现优势:无需修改
MainMemento的任何实现,支持灵活配置比较规则,比如忽略特定字段、自定义某类属性的比较逻辑 - 实现示例:
[Test] public void TestMainMementoComparison() { Tuple<MainMemento, MainMemento> data = SaveLoad(); // 直接使用断言库的深度比较能力,不需要修改业务类代码 Assert.That(data.Item1, Is.EqualTo(data.Item2).UsingDeepComparison()); }
主流的.NET测试断言库都原生支持该能力,可直接使用。
注意事项
你当前的实现存在逻辑风险:自定义的Equals方法和GetHashCode逻辑不一致,若将MainMemento作为字典Key、存入HashSet等哈希容器时会出现逻辑错误,若保留现有自定义Equals实现,必须同步重写GetHashCode方法,基于所有参与比较的属性计算哈希值。
内容的提问来源于stack exchange,提问作者Mark
相关产品推荐
相关产品推荐

