Единичные тесты для глубокого клонирования

Скажем, у меня есть сложный класс .NET с множеством массивов и другими членами класса. Я должен иметь возможность генерировать глубокий клон этого объекта, поэтому я пишу метод Clone() и реализую его с помощью простого BinaryFormatter serialize/deserialize - или, возможно, я делаю глубокий клон, используя другую технику, которая более подвержена ошибкам и я бы хотел убедиться, что это проверено.

ОК, так что теперь (нормально, я должен был сделать это сначала). Мне бы хотелось написать тесты, которые охватывают клонирование. Все члены класса являются частными, и моя архитектура настолько хороша (!), Что мне не нужно было писать сотни публичных свойств или других аксессуаров. Класс не является IComparable или IEquatable, потому что это не требуется приложению. Мои модульные тесты находятся в отдельной сборке с производственным кодом.

Какие подходы люди прибегают к тестированию, что клонированный объект является хорошей копией? Вы пишете (или переписываете, как только обнаруживаете потребность в клоне) все ваши модульные тесты для класса, чтобы их можно было вызвать с помощью "девственного" объекта или с помощью клона? Как бы вы протестировали, если часть клонирования была недостаточно глубокой - так как это только проблема, которая может дать отвратительные ошибки? "

Ответ 1

Ваш метод тестирования будет зависеть от типа решения, которое вы придумали. Если вы пишете какой-то пользовательский код клонирования и должны вручную реализовать это в каждом клонируемом типе, вы должны действительно протестировать клонирование каждого из этих типов. В качестве альтернативы, если вы решите перейти на более общий маршрут (где, вероятно, будет указано вышеупомянутое отражение), ваши тесты должны были бы только протестировать конкретные сценарии, к которым вам придется клонировать систему.

Чтобы ответить на ваши конкретные вопросы:

Вы пишете (или переписываете, как только обнаруживаете необходимость для клона) все ваши модульные тесты для класса, чтобы они могли быть вызваны либо с помощью "девственного" объекта, либо с его клоном?

У вас должны быть тесты для всех методов, которые могут выполняться как на оригинальном, так и на клонированном объекте. Обратите внимание, что довольно просто настроить простой тестовый проект для поддержки этого без ручного обновления логики для каждого теста.

Как бы вы протестировали, если часть клонирования не была достаточно глубокой - так как это только проблема, которая может дать отвратительные ошибки позже?

Это зависит от выбранного вами метода клонирования. Если вам нужно вручную обновлять типы с клонированием, тогда вы должны проверить, что каждый тип клонирует все (и только) членов, которых вы ожидаете. Принимая во внимание, что, если вы тестируете структуру клонирования, я бы создал несколько тестовых клонированных типов для тестирования каждого сценария, который вам нужно поддерживать.

Ответ 2

Там действительно очевидное решение, которое не требует почти такой же работы:

  • Сериализовать объект в двоичном формате.
  • Клонировать объект.
  • Сериализовать клон в двоичном формате.
  • Сравните байты.

Предполагая, что сериализация работает - и это лучше, потому что вы используете ее для клонирования - это должно быть легко поддерживать. Фактически, он будет полностью инкапсулирован из изменений в структуру вашего класса.

Ответ 3

Я бы просто написал один тест, чтобы определить, был ли клон правильным или нет. Если класс не запечатан, вы можете создать жгут для него, расширив его, а затем разоблачив все свои внутренние элементы в дочернем классе. В качестве альтернативы вы можете использовать отражение (yech) или использовать генераторы MSTest Accessor.

Вам нужно клонировать ваш объект, а затем пройти через каждое свойство и переменную, которые есть у вашего объекта, и определить, правильно ли они были скопированы или правильно клонированы.

Ответ 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));
                    }
                }
            }
        }
    }