Юнит-тесты для глубокого клонирования - PullRequest
10 голосов
/ 14 августа 2008

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

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

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

Ответы [ 6 ]

2 голосов
/ 23 марта 2010

Существует действительно очевидное решение, которое не требует такой большой работы:

  1. Сериализация объекта в двоичном формате.
  2. Клонировать объект.
  3. Сериализация клона в двоичном формате.
  4. Сравните байты.

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

2 голосов
/ 14 августа 2008

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

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

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

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

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

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

1 голос
/ 18 сентября 2012

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

1 голос
/ 23 августа 2008

Мне нравится писать модульные тесты, использующие один из встроенных сериализаторов исходного и клонированного объекта, а затем проверять сериализованные представления на равенство (для двоичного форматера я могу просто сравнить байтовые массивы). Это прекрасно работает в тех случаях, когда объект по-прежнему сериализуем, и я перехожу только на пользовательский глубокий клон по соображениям производительности.

Кроме того, я хотел бы добавить проверку режима отладки для всех моих реализаций 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 голос
/ 14 августа 2008

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

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

0 голосов
/ 30 июня 2015

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

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));
                    }
                }
            }
        }
    }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...