Teste unitare pentru clonarea profundă

Să presupunem că am o clasă complexă .NET, cu o mulțime de matrice și cu alți membri ai obiectului de clasă. Trebuie să pot genera o clonă profundă a acestui obiect - așa că scriu o metodă Clone() și o implementez cu o simulare BinaryFormatter serialize/deserialize - sau poate fac clona adâncă utilizând o altă tehnică care este mai predispusă la erori și aș vrea să fiu sigur că este testat.

OK, deci acum (bine, ar fi trebuit să o fac mai întâi) Aș vrea să scriu teste care acoperă clonarea. Toți membrii clasei sunt privați, iar arhitectura mea este atât de bună (!) Încât nu am avut nevoie să scriu sute de proprietăți publice sau alți accesori. Clasa nu este IComparabilă sau IEquatable, deoarece nu este necesară de aplicație. Testele unității mele se află într-un ansamblu separat de codul de producție.

Ce abordări iau oamenii pentru a testa că obiectul clonat este o copie bună? Scrieți (sau rescrieți odată ce ați descoperit necesitatea clonării) toate testele unității pentru clasă, astfel încât acestea să poată fi invocate cu fie un obiect virgin < em> sau cu o clonă a acestuia? Cum ați testa dacă o parte din clonare nu a fost destul de adâncă - deoarece acesta este doar un fel de problemă care poate da bug-uri hideous-to-find mai târziu?

0
fr hi bn

5 răspunsuri

Aș scrie un singur test pentru a determina dacă clona a fost corectă sau nu. În cazul în care clasa nu este sigilată, puteți crea un ham pentru aceasta prin extinderea acesteia, și apoi expunerea tuturor internelor în cadrul clasei copil. Alternativ, ați putea folosi reflexia (yech) sau utilizați generatoarele Accessor de la MSTest.

Trebuie să vă clonați obiectul și apoi să treceți prin fiecare proprietate și variabilă a obiectului dvs. și să determinați dacă a fost copiat corect sau clonat corect.

0
adăugat

În mod normal, aș fi implementat Equals() pentru a compara cele două obiecte în profunzime. S-ar putea să nu aveți nevoie de el în codul de producție, dar s-ar putea să vină în continuare la îndemână mai târziu și codul de testare este mult mai curat.

0
adăugat

Metoda de testare va depinde de tipul de soluție pe care o găsiți. Dacă scrieți un anumit cod de clonare personalizat și trebuie să îl implementați manual în fiecare tip clonabil, atunci ar trebui să testați cu adevărat clonarea fiecăruia dintre aceste tipuri. În mod alternativ, dacă vă decideți să mergeți la un traseu mai generic (în cazul în care reflexia menționată anterior se va potrivi probabil), testele dvs. ar trebui doar să testeze scenariile specifice cu care va trebui să faceți față sistemul de clonare.

Pentru a răspunde la întrebările dvs. specifice:

Încercați să scrieți (sau să rescrieți după ce ați descoperit necesitatea clonării) toate testele unității pentru clasă, astfel încât acestea să poată fi invocate cu un obiect "virgin" sau cu o clonă a acestuia?

Ar trebui să aveți teste pentru toate metodele care pot fi efectuate atât pe obiectele originale cât și pe cele clonate. Rețineți că ar trebui să fie destul de ușor să configurați un simplu test pentru a sprijini acest lucru fără a actualiza manual logica pentru fiecare test.

Cum ați testa dacă o parte din clonare nu a fost destul de adâncă - căci acesta este doar un fel de problemă care poate da bug-uri greu de găsit mai târziu?

Depinde de metoda de clonare pe care o alegeți. Dacă trebuie să actualizați manual tipurile de clona, ​​trebuie să testați că fiecare tip clonizează toate (și numai) membrii pe care îi așteptați. În timp ce, dacă testați un cadru de clonare, aș crea câteva tipuri de clonare pentru a testa fiecare scenariu pe care trebuie să îl sprijiniți.

0
adăugat

Iată un exemplu despre modul în care am implementat acest lucru înapoi, deși acest lucru va trebui adaptat scenariului. În acest caz, am avut un lanț neplăcut de obiecte care s-ar putea schimba cu ușurință și clona a fost folosită ca o implementare extrem de critică a prototipului, așa că a trebuit să corectez acest test împreună.

public static class TestDeepClone
    {
        private static readonly List objectIDs = new List();
        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;
        }

        /// 
        /// Executes various assertions to ensure the validity of a deep copy for any object including its compositions
        /// 
        /// The original object
        /// The cloned object
        /// A predicate for any exclusions to be done, i.e not to expect IPolicy items to be cloned
        public static void AssertDeepClone(this Object original, Object copy, Predicate 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 items = copyValueEnumerable.Cast().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 items = copyValueEnumerable.Cast().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));
                    }
                }
            }
        }
    }


0
adăugat

Îmi place să scriu teste unitare care utilizează unul dintre serializatorii încorporați pe obiectul original și obiectul clonat și apoi să verifice reprezentările serializate pentru egalitate (pentru un formatator binar, pot compara doar tablourile de octeți). Acest lucru funcționează foarte bine în cazurile în care obiectul este încă serializabil și schimb doar o clonă adâncă personalizată din motive perf.

În plus, îmi place să adaug o verificare a modului de depanare la toate implementările mele Clone folosind ceva de genul asta

[Conditional("DEBUG")]
public static void DebugAssertValueEquality(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(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));
}

Această metodă se bazează pe o implementare corectă a Equals pe toți membrii imbricați, dar în cazul meu tot ce este clonabil este de asemenea echivalent

0
adăugat