关于c#:用于深度克隆的单元测试

关于c#:用于深度克隆的单元测试

Unit tests for deep cloning

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

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

人们采用什么方法来测试克隆对象是否是好的副本?您是否为该类编写了所有单元测试(或在发现克隆需求后重新编写),以便可以使用"原始"对象或其克隆来调用它们?您将如何测试部分克隆是否不够深入-因为这只是一种问题,以后可能会出现难以发现的错误?


有一个非常明显的解决方案,它不需要花费那么多的工作:

  • 将对象序列化为二进制格式。
  • 克隆对象。
  • 将克隆序列化为二进制格式。
  • 比较字节。
  • 假设序列化有效-并且因为您正在使用它进行克隆而更好-这应该易于维护。实际上,它将完全封装在类结构的更改中。


    您的测试方法取决于您提出的解决方案类型。如果编写一些自定义克隆代码,并且必须在每种可克隆类型中手动实现该代码,那么您应该真正测试这些类型中的每一种的克隆。另外,如果您决定走一条更通用的路线(前面提到的反射很可能会适合),您的测试仅需要测试克隆系统必须处理的特定情况。

    要回答您的特定问题:

    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?

    这取决于您选择的克隆方法。如果必须手动更新可克隆类型,则应测试每种类型是否正在克隆您期望的所有(且仅)成员。而如果您正在测试克隆框架,则将创建一些测试可克隆类型以测试您需要支持的每种方案。


    我通常会实现Equals()来深度比较两个对象。您可能不需要在生产代码中使用它,但以后可能仍会派上用场,并且测试代码更加简洁。


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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    [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的准确实现,但是在我的情况下,任何可克隆的东西也都是相等的


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

    您需要克隆对象,然后遍历对象具有的每个属性和变量,并确定它是正确复制还是正确克隆。


    这是我前一段时间如何实现此示例,尽管这需要根据情况进行定制。在这种情况下,我们有一个讨厌的对象链,可以轻松更改,并将克隆用作非常关键的原型实现,因此我不得不将该测试打补丁(修改)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    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));
                        }
                    }
                }
            }
        }


    推荐阅读