深度克隆的单元测试
假设我有一个复杂的.NET类,其中包含许多数组和其他类对象成员。我需要能够生成此对象的深层克隆,因此我编写了一个Clone()方法,并使用简单的BinaryFormatter序列化/反序列化来实现它,或者我可能使用其他一些更容易出错的技术来进行深层克隆想确保已通过测试。
好的,所以现在(好的,我应该先这样做),我想编写涵盖克隆的测试。该类的所有成员都是私有的,而且我的体系结构是如此好(!),以至于我不需要编写数百个公共属性或者其他访问器。该类不是IComparable或者IEquatable,因为应用程序不需要该类。我的单元测试与生产代码在单独的程序集中。
人们采用什么方法来测试克隆对象是否是好的副本?我们是否为该类编写了所有单元测试(或者一旦发现克隆的需求就进行了重写),以便可以使用"原始"对象或者该对象的克隆来调用它们?我们将如何测试部分克隆是否不够深入,因为这只是一种问题,以后可能会出现难以发现的错误?
解决方案
回答
我只是编写一个测试来确定克隆是否正确。如果未密封该类,则可以通过扩展该类,然后在子类中公开所有内部组件,为其创建线束。或者,我们可以使用反射(yech),或者使用MSTest的Accessor生成器。
我们需要克隆对象,然后遍历对象具有的每个属性和变量,并确定它是正确复制还是正确克隆。
回答
测试方法取决于我们提出的解决方案类型。如果我们编写一些自定义克隆代码,并且必须在每种可克隆类型中手动实现该代码,那么我们应该真正测试这些类型中的每一种的克隆。另外,如果我们决定走一条更通用的路线(前面提到的反射很可能会适合),测试仅需要测试克隆系统必须处理的特定情况。
要回答特定问题:
Do you write (or rewrite once you discover the need for the clone) all your unit tests for the class so that they can be invoked with either a 'virgin' object or with a clone of it?
我们应该对可以在原始对象和克隆对象上执行的所有方法进行测试。请注意,无需手动更新每个测试的逻辑就可以很容易地设置一个简单的测试设计来支持该设计。
How would you test if part of the cloning wasn't deep enough - as this is just the kind of problem which can give hideous-to-find bugs later?
这取决于我们选择的克隆方法。如果必须手动更新可克隆类型,则应测试每种类型是否正在克隆所有(且仅)期望的成员。而如果我们正在测试克隆框架,我将创建一些测试可克隆类型来测试我们需要支持的每种方案。
回答
我喜欢编写在原始对象和克隆对象上使用内置序列化程序之一的单元测试,然后检查序列化表示形式的相等性(对于二进制格式化程序,我可以比较字节数组)。在对象仍可序列化的情况下,这非常有用,并且出于性能方面的考虑,我仅更改为自定义深度克隆。
此外,我喜欢使用类似的方法向所有克隆实现中添加调试模式检查
[Conditional("DEBUG")] public static void DebugAssertValueEquality<T>(T current, T other, bool expected, params string[] ignoredFields) { if (null == current) { throw new ArgumentNullException("current"); } if (null == ignoredFields) { ignoredFields = new string[] { }; } FieldInfo lastField = null; bool test; if (object.ReferenceEquals(other, null)) { Debug.Assert(false == expected, "The other object was null"); return; } test = true; foreach (FieldInfo fi in current.GetType().GetFields(BindingFlags.Instance)) { if (test = false) { break; } if (0 <= Array.IndexOf<string>(ignoredFields, fi.Name)) { continue; } lastField = fi; object leftValue = fi.GetValue(current); object rightValue = fi.GetValue(other); if (object.ReferenceEquals(null, leftValue)) { if (!object.ReferenceEquals(null, rightValue)) { test = false; } } else if (object.ReferenceEquals(null, rightValue)) { test = false; } else { if (!leftValue.Equals(rightValue)) { test = false; } } } Debug.Assert(test == expected, string.Format("field: {0}", lastField)); }
此方法依赖于对任何嵌套成员的Equals的准确实现,但是在我的情况下,任何可克隆的东西也是相等的
回答
有一个非常明显的解决方案,它不需要花费那么多的工作:
- 将对象序列化为二进制格式。
- 克隆对象。
- 将克隆序列化为二进制格式。
- 比较字节。
假定序列化有效并且更好,因为我们正在使用它来克隆它,因此应该易于维护。实际上,它将完全封装在类结构的更改中。