C# 深度克隆的单元测试

C# 深度克隆的单元测试,c#,.net,unit-testing,C#,.net,Unit Testing,假设我有一个复杂的.NET类,有很多数组和其他类对象成员。我需要能够生成此对象的深度克隆-因此我编写了一个clone()方法,并使用简单的BinaryFormatter序列化/反序列化来实现它-或者我使用其他更容易出错的技术进行深度克隆,并且我希望确保已测试 好的,现在(好的,我应该先做)我想写一些关于克隆的测试。这个类的所有成员都是私有的,我的体系结构非常好(!),我不需要编写数百个公共属性或其他访问器。该类不是IComparable或IEquatable,因为应用程序不需要它。我的单元测试在

假设我有一个复杂的.NET类,有很多数组和其他类对象成员。我需要能够生成此对象的深度克隆-因此我编写了一个clone()方法,并使用简单的BinaryFormatter序列化/反序列化来实现它-或者我使用其他更容易出错的技术进行深度克隆,并且我希望确保已测试

好的,现在(好的,我应该先做)我想写一些关于克隆的测试。这个类的所有成员都是私有的,我的体系结构非常好(!),我不需要编写数百个公共属性或其他访问器。该类不是IComparable或IEquatable,因为应用程序不需要它。我的单元测试在生产代码的单独程序集中


人们采取什么方法来测试克隆对象是否是一个好的副本?您是否编写(或在发现需要克隆时重写)类的所有单元测试,以便可以使用“virgin”对象或其克隆调用它们?如果克隆的一部分不够深入,您将如何测试?因为这正是一种问题,以后可能会让人很难找到bug?

我只编写一个测试来确定克隆是否正确。如果类未密封,则可以通过扩展它,然后在子类中公开所有内部构件来为其创建线束。或者,您可以使用反射(yech),或者使用MSTest的访问器生成器


您需要克隆对象,然后检查对象的每个属性和变量,并确定是否正确复制或克隆了对象

您的测试方法将取决于您提出的解决方案类型。如果您编写了一些自定义克隆代码,并且必须在每个可克隆类型中手动实现这些代码,那么您应该真正测试这些类型中每个类型的克隆。或者,如果您决定采用一种更通用的方法(上面提到的反射可能适合这种方法),那么您的测试只需要测试克隆系统必须处理的特定场景

要回答您的具体问题:

您是否编写(或在发现需要克隆时重写)类的所有单元测试,以便可以使用“virgin”对象或其克隆调用它们

您应该对可以在原始对象和克隆对象上执行的所有方法进行测试。请注意,在不手动更新每个测试的逻辑的情况下,设置一个简单的测试设计来支持这一点应该非常容易

如果克隆的一部分不够深入,你会如何测试呢?因为这正是一种问题,它会让你在以后发现bug时变得可怕


这取决于您选择的克隆方法。如果您必须手动更新可克隆类型,那么您应该测试每个类型是否正在克隆所有(并且仅克隆)您期望的成员。然而,如果您正在测试克隆框架,我会创建一些测试可克隆类型来测试您需要支持的每个场景。

我喜欢编写单元测试,在原始对象和克隆对象上使用一个内置序列化器,然后检查序列化表示是否相等(对于二进制格式化程序,我可以比较字节数组)。这在对象仍然可以序列化的情况下非常有效,并且我只是出于性能原因才更改为自定义深度克隆

此外,我喜欢使用类似这样的方法将调试模式检查添加到所有克隆实现中

[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));
}
[条件(“调试”)]
public static void DebugAssertValueEquality(T current,T other,bool预期值,
参数字符串[]ignoredFields){
如果(空==当前)
{抛出新的ArgumentNullException(“当前”);}
if(null==ignoredFields)
{ignoredFields=新字符串[]{};}
FieldInfo lastField=null;
布尔试验;
if(object.ReferenceEquals(other,null))
{Debug.Assert(false==预期,“另一个对象为null”);return;}
测试=真;
foreach(当前.GetType().GetFields(BindingFlags.Instance)中的FieldInfo fi){
如果(test=false){break;}

如果(0有一个非常明显的解决方案,它几乎不需要那么多的工作:

  • 将对象序列化为二进制格式
  • 克隆对象
  • 将克隆序列化为二进制格式
  • 比较字节

  • 假设序列化可以工作,并且因为您使用它来克隆,所以它会更好,这应该很容易维护。事实上,它将从对类结构的更改中完全封装起来。

    我通常会实现
    Equals()
    用于深入比较这两个对象。您可能不需要在生产代码中使用它,但它可能在以后会派上用场,测试代码也会更干净。

    下面是我前一段时间如何实现这一点的一个示例,尽管这需要根据场景进行定制。在这种情况下,我们有一个令人讨厌的对象链,很容易改变nge和克隆被用作一个非常关键的原型实现,所以我不得不一起修补(破解)这个测试

    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));
                        }
                    }
                }
            }
        }
    
    公共静态类TestDeepClone
    {
    私有静态只读列表objectId=new List();
    私有静态只读ObjectGenerator ObjectGenerator=新ObjectGenerator();
    公共静态bool defaultCloneExclusionCheck(对象obj)
    {
    返回
    obj是ValueType||
    obj是字符串||
    obj是代表||
    obj是可数的;
    }
    /// 
    ///执行各种断言,以确保任何对象(包括其组成)的深度副本的有效性
    /// 
    ///原物
    ///克隆对象
    ///任何要执行的排除的谓词,即不期望IPolicy项为clo