用于深度克隆的单元测试

Posted

技术标签:

【中文标题】用于深度克隆的单元测试【英文标题】:Unit tests for deep cloning 【发布时间】:2008-08-14 12:49:03 【问题描述】:

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

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

人们采用什么方法来测试克隆的对象是否是好的副本?您是否为该类编写(或重写一旦发现需要克隆)所有单元测试,以便可以使用 either '处女'对象调用它们或有它的克隆?如果部分克隆的深度不够深,您将如何测试 - 因为这种问题可能会在以后给您带来难以发现的错误?

【问题讨论】:

【参考方案1】:

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

回答您的具体问题:

您是否为该类编写(或在发现需要克隆后重写)所有单元测试,以便可以使用“原始”对象或它的克隆来调用它们?

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

如果部分克隆的深度不够,您将如何测试 - 因为这只是一种问题,可能会导致以后难以发现错误?

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

【讨论】:

【参考方案2】:

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

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

假设序列化有效 - 并且更好,因为您使用它来克隆 - 这应该很容易维护。事实上,它会从你的类结构的变化中完全封装起来。

【讨论】:

【参考方案3】:

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

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

【讨论】:

【参考方案4】:

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

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

[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 的准确实现,但在我的情况下,任何可克隆的东西也是可相等的

【讨论】:

【参考方案5】:

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

【讨论】:

【参考方案6】:

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

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

【讨论】:

以上是关于用于深度克隆的单元测试的主要内容,如果未能解决你的问题,请参考以下文章

用于测试 rxjava 的书面单元测试,但不确定我的单元测试是不是正在测试所有内容

单元测试WooCommerce扩展

Spring 的 MockMvc 是用于单元测试还是集成测试?

用于单元测试的 UserDefaults 实例?

用于链式函数调用的 Jest 单元测试

用于单元测试的内存 DBMS