C# 深度克隆的单元测试
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/10949/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Unit tests for deep cloning
提问by Will Dean
Let's say I have a complex .NET class, with lots of arrays and other class object members. I need to be able to generate a deep clone of this object - so I write a Clone() method, and implement it with a simple BinaryFormatter serialize/deserialize - or perhaps I do the deep clone using some other technique which is more error prone and I'd like to make sure is tested.
假设我有一个复杂的 .NET 类,有很多数组和其他类对象成员。我需要能够生成此对象的深度克隆 - 所以我编写了一个 Clone() 方法,并使用简单的 BinaryFormatter 序列化/反序列化来实现它 - 或者我可能使用其他一些更容易出错的技术进行深度克隆我想确保经过测试。
OK, so now (ok, I should have done it first) I'd like write tests which cover the cloning. All the members of the class are private, and my architecture is so good (!) that I haven't needed to write hundreds of public properties or other accessors. The class isn't IComparable or IEquatable, because that's not needed by the application. My unit tests are in a separate assembly to the production code.
好的,现在(好的,我应该先完成)我想编写涵盖克隆的测试。类的所有成员都是私有的,我的架构非常好(!),我不需要编写数百个公共属性或其他访问器。该类不是 IComparable 或 IEquatable,因为应用程序不需要它们。我的单元测试与生产代码在一个单独的程序集中。
What approaches do people take to testing that the cloned object is a good copy? Do you write (or rewriteonce you discover the need for the clone) all your unit tests for the class so that they can be invoked with eithera 'virgin' object orwith 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?
人们采取什么方法来测试克隆对象是否是好的副本?您是否编写(或在发现需要克隆时重写)类的所有单元测试,以便可以使用“原始”对象或它的克隆来调用它们?如果克隆的一部分不够深,您将如何测试 - 因为这只是一种可能会在以后发现可怕的错误的问题?
回答by Will Dean
I'd just write a single test to determine if the clone was correct or not. If the class isn't sealed, you can create a harness for it by extending it, and then exposing all your internals within the child class. Alternatively, you could use reflection (yech), or use MSTest's Accessor generators.
我只是编写一个测试来确定克隆是否正确。如果该类不是密封的,您可以通过扩展它来为它创建一个线束,然后在子类中公开您的所有内部结构。或者,您可以使用反射 (yech),或使用 MSTest 的访问器生成器。
You need to clone your object and then go through every single property and variable that your object has and determine if it was copied correctly or cloned correctly.
您需要克隆对象,然后检查对象具有的每个属性和变量,并确定它是被正确复制还是被正确克隆。
回答by akmad
You method of testing will depend on the type of solution you come up with. If you write some custom cloning code and have to manually implement that in each cloneable type then you should really test the cloning of each one of those types. Alternatively, if you decide to go a more generic route (where the aforementioned reflection would likely fit in), your tests would only need to test the specific scenarios that you cloning system will have to deal with.
您的测试方法将取决于您提出的解决方案类型。如果您编写了一些自定义克隆代码并且必须在每个可克隆类型中手动实现它,那么您应该真正测试这些类型中的每一种的克隆。或者,如果您决定走一条更通用的路线(前面提到的反射可能适合的地方),您的测试只需要测试您克隆系统必须处理的特定场景。
To answer your specific questions:
要回答您的具体问题:
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?
您是否编写(或在发现需要克隆时重写)类的所有单元测试,以便可以使用“原始”对象或它的克隆来调用它们?
You should have tests for all the methods that can be performed on both the original and cloned objects. Note that it should be pretty easy to set up a simple test design to support this without manually updating the logic for each test.
您应该对可以在原始对象和克隆对象上执行的所有方法进行测试。请注意,设置一个简单的测试设计来支持这一点应该非常容易,而无需手动更新每个测试的逻辑。
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?
如果克隆的一部分不够深,您将如何测试 - 因为这只是一种可能会在以后发现可怕的错误的问题?
It depends on the cloning method you choose. If you have to manually update the cloneable types then you should test that each type is cloning all (and only) the members you expect. Whereas, if you are testing a cloning framework I would create some test cloneable types to test each scenario you need to support.
这取决于您选择的克隆方法。如果您必须手动更新可克隆类型,那么您应该测试每种类型是否克隆了您期望的所有(且仅)成员。然而,如果您正在测试克隆框架,我将创建一些测试可克隆类型来测试您需要支持的每个场景。
回答by Andrew
I like to write unit tests that use one of the builtin serializers on the original and the cloned object and then check the serialized representations for equality (for a binary formatter, I can just compare the byte arrays). This works great in cases where the object is still serializable, and I'm only changing to a custom deep clone for perf reasons.
我喜欢编写单元测试,在原始对象和克隆对象上使用内置序列化程序之一,然后检查序列化表示的相等性(对于二进制格式化程序,我可以只比较字节数组)。这在对象仍可序列化的情况下非常有效,并且我只是出于性能原因更改为自定义深度克隆。
Furthermore, I like to add a debug mode check to all of my Clone implementations using something like this
此外,我喜欢使用这样的东西向我的所有克隆实现添加调试模式检查
[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));
}
This method relies on an accurate implementation of Equals on any nested members, but in my case anything that is cloneable is also equatable
这种方法依赖于对任何嵌套成员的 Equals 的准确实现,但在我的情况下,任何可克隆的东西也是可等同的
回答by Andrew
There's a really obvious solution that doesn't take nearly as much work:
有一个非常明显的解决方案,几乎不需要那么多工作:
- Serialize the object into a binary format.
- Clone the object.
- Serialize the clone into a binary format.
- Compare the bytes.
- 将对象序列化为二进制格式。
- 克隆对象。
- 将克隆序列化为二进制格式。
- 比较字节。
Assuming that serialization works - and it better because you are using it to clone - this should be easy to maintain. In fact, it will be encapsulated from changes to the structure of your class completely.
假设序列化有效 - 并且它更好,因为您使用它来克隆 - 这应该很容易维护。事实上,它会从你的类结构的变化中完全被封装起来。
回答by EricSchaefer
I would usually implement Equals()
for comparing the two objects in depth. You might not need it in your production code but it might still come in handy later and the test code is much cleaner.
我通常会实现Equals()
深入比较两个对象。您可能在生产代码中不需要它,但它可能在以后仍然派上用场,并且测试代码更清晰。
回答by Thulani Chivandikwa
Here is a sample of how I implemented this a while back, although this will need to be tailored to the scenario. In this case we had a nasty object chain that could easily change and the clone was used as a very critical prototype implementation and so I had to patch (hack) this test together.
这是我不久前如何实现的示例,尽管这需要根据场景进行定制。在这种情况下,我们有一个很容易改变的讨厌的对象链,克隆被用作非常关键的原型实现,所以我不得不修补(破解)这个测试。
public static class TestDeepClone
{
private static readonly List<long> objectIDs = new List<long>();
private static readonly ObjectIDGenerator objectIdGenerator = new ObjectIDGenerator();
public static bool DefaultCloneExclusionsCheck(Object obj)
{
return
obj is ValueType ||
obj is string ||
obj is Delegate ||
obj is IEnumerable;
}
/// <summary>
/// Executes various assertions to ensure the validity of a deep copy for any object including its compositions
/// </summary>
/// <param name="original">The original object</param>
/// <param name="copy">The cloned object</param>
/// <param name="checkExclude">A predicate for any exclusions to be done, i.e not to expect IPolicy items to be cloned</param>
public static void AssertDeepClone(this Object original, Object copy, Predicate<object> checkExclude)
{
bool isKnown;
if (original == null) return;
if (copy == null) Assert.Fail("Copy is null while original is not", original, copy);
var id = objectIdGenerator.GetId(original, out isKnown); //Avoid checking the same object more than once
if (!objectIDs.Contains(id))
{
objectIDs.Add(id);
}
else
{
return;
}
if (!checkExclude(original))
{
Assert.That(ReferenceEquals(original, copy) == false);
}
Type type = original.GetType();
PropertyInfo[] propertyInfos = type.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
FieldInfo[] fieldInfos = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
foreach (PropertyInfo memberInfo in propertyInfos)
{
var getmethod = memberInfo.GetGetMethod();
if (getmethod == null) continue;
var originalValue = getmethod.Invoke(original, new object[] { });
var copyValue = getmethod.Invoke(copy, new object[] { });
if (originalValue == null) continue;
if (!checkExclude(originalValue))
{
Assert.That(ReferenceEquals(originalValue, copyValue) == false);
}
if (originalValue is IEnumerable && !(originalValue is string))
{
var originalValueEnumerable = originalValue as IEnumerable;
var copyValueEnumerable = copyValue as IEnumerable;
if (copyValueEnumerable == null) Assert.Fail("Copy is null while original is not", new[] { original, copy });
int count = 0;
List<object> items = copyValueEnumerable.Cast<object>().ToList();
foreach (object o in originalValueEnumerable)
{
AssertDeepClone(o, items[count], checkExclude);
count++;
}
}
else
{
//Recurse over reference types to check deep clone success
if (!checkExclude(originalValue))
{
AssertDeepClone(originalValue, copyValue, checkExclude);
}
if (originalValue is ValueType && !(originalValue is Guid))
{
//check value of non reference type
Assert.That(originalValue.Equals(copyValue));
}
}
}
foreach (FieldInfo fieldInfo in fieldInfos)
{
var originalValue = fieldInfo.GetValue(original);
var copyValue = fieldInfo.GetValue(copy);
if (originalValue == null) continue;
if (!checkExclude(originalValue))
{
Assert.That(ReferenceEquals(originalValue, copyValue) == false);
}
if (originalValue is IEnumerable && !(originalValue is string))
{
var originalValueEnumerable = originalValue as IEnumerable;
var copyValueEnumerable = copyValue as IEnumerable;
if (copyValueEnumerable == null) Assert.Fail("Copy is null while original is not", new[] { original, copy });
int count = 0;
List<object> items = copyValueEnumerable.Cast<object>().ToList();
foreach (object o in originalValueEnumerable)
{
AssertDeepClone(o, items[count], checkExclude);
count++;
}
}
else
{
//Recurse over reference types to check deep clone success
if (!checkExclude(originalValue))
{
AssertDeepClone(originalValue, copyValue, checkExclude);
}
if (originalValue is ValueType && !(originalValue is Guid))
{
//check value of non reference type
Assert.That(originalValue.Equals(copyValue));
}
}
}
}
}