深度克隆的单元测试

11
假设我有一个复杂的.NET类,它有很多数组和其他类对象成员。我需要能够生成该对象的深层克隆——因此我编写了一个Clone()方法,并使用简单的BinaryFormatter序列化/反序列化实现它——或者我使用其他更容易出错并想要确保测试的技术来进行深层克隆。
好的,现在(好吧,我应该先这样做)我想编写覆盖克隆的测试。类的所有成员都是私有的,并且我的架构非常好(!),我不需要编写数百个公共属性或其他访问器。该类不是IComparable或IEquatable,因为应用程序不需要。我的单元测试位于与生产代码分开的程序集中。
人们采取哪些方法来测试克隆对象是否良好?您是否编写(或在发现需要克隆时重写)类的所有单元测试,以便可以使用其“原始”对象它的克隆调用它们?如果克隆的一部分不够深,您将如何进行测试-因为这正是可能会在后面导致难以找到的错误的问题?
6个回答

2

有一个非常明显的解决方案,不需要太多的工作:

  1. 将对象序列化为二进制格式。
  2. 克隆对象。
  3. 将克隆对象序列化为二进制格式。
  4. 比较字节。

假设序列化正常工作 - 而且它应该正常工作,因为你正在使用它来进行克隆 - 这应该很容易维护。实际上,它将完全封装对类结构的更改。


2
你的测试方法将取决于你提出的解决方案类型。如果你编写一些自定义克隆代码,并且必须在每个可克隆类型中手动实现它,那么你应该确保测试每个类型的克隆。或者,如果你决定采用更通用的方法(其中前面提到的反射可能适用),则你的测试只需要测试克隆系统将要处理的特定情况。
回答你的具体问题: 你应该为所有可以在原始对象和克隆对象上执行的方法编写测试。请注意,设置支持此功能的简单测试设计应该相当容易,而不需要手动更新每个测试的逻辑。
如果克隆的某个部分不够深,如何测试?这取决于你选择的克隆方法。如果你必须手动更新可克隆类型,则应测试每种类型是否克隆了你期望的所有成员(且仅限这些成员)。而如果你正在测试一个克隆框架,我会创建一些测试可克隆类型来测试你需要支持的每种情况。

1

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

此外,我喜欢在所有Clone实现中添加调试模式检查,类似于以下内容

[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的准确实现,但在我的情况下,任何可克隆的对象也是可比较的。


1

我会写一个单一的测试来确定克隆是否正确。如果类没有被密封,你可以通过扩展它并在子类中公开所有内部来为它创建一个测试工具。或者,你可以使用反射(不太好),或使用MSTest的访问器生成器。

你需要克隆对象,然后遍历每个属性和变量,确定它们是否被正确复制或克隆。


1

通常我会实现Equals()方法来深度比较两个对象。在生产代码中可能不需要,但以后可能仍然有用,而且测试代码更加清晰。


0

这是我一段时间前实现此功能的示例,尽管需要根据具体情况进行调整。在这种情况下,我们有一个非常复杂的对象链,可能会经常变化,而克隆作为一个非常关键的原型实现被使用,因此我不得不拼凑(hack)这个测试。

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));
                    }
                }
            }
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接