AntonioGanci

Il blog di Antonio Ganci
posts - 201, comments - 420, trackbacks - 31

Alcuni approcci per testare il valore di un oggetto che non sia un value type in uno unit test

In questo post analizzerò il problema di testare il risultato di un operazione che ritorni un oggetto complesso anzichè un semplice value type (int, double, ...), nel post propongo una serie di possbili soluzioni.

Supponiamo di avere una classe che calcola alcune statistiche su una serie di valori:

    class StatisticCalculator

    {

        public StatisticValues Calculate(double[] values)

        {

            StatisticValues result = new StatisticValues();

            // ...

            return result;

        }

    }

(Questa classe soffre di un problema di design, il metodo Calculate fa troppe cose insieme; in un progetto reale avrei creato un metodo diverso per ogni statistica)

E di voler testare il metodo Calculate:

    [TestFixture]

    public class StatisticCalculatorTests

    {

        [Test]

        public void Test1()

        {

            StatisticCalculator calculator = new StatisticCalculator();

            StatisticValues result = calculator.Calculate(

                new double[] {1, 2, 3});

            StatisticValues expectedResult = new StatisticValues();

            expectedResult.Count = 3;

            expectedResult.Minimum = 1;

            expectedResult.Maximum = 3;

            expectedResult.Mean = 2;

            Assert.AreEqual(expectedResult, result);

        }

    }

Se eseguiamo il test, supponendo che il codice del metodo Calculate sia corretto, l'Assert fallisce. Il motivo è che anche se i valori delle due classi sono uguali la Assert.AreEqual usa il metodo Equals, il quale di default considera due classi uguali se le variabili referenziano la stessa instanza.

Una prima soluzione a questo problema potrebbe essere quella di scrivere un Assert per ogni property della classe StatisticValue:

        [Test]

        public void Test1()

        {

            StatisticCalculator calculator = new StatisticCalculator();

            StatisticValues result = calculator.Calculate(

                new double[] {1, 2, 3});

            Assert.AreEqual(3, result.Count);

            Assert.AreEqual(1, result.Minimum);

            Assert.AreEqual(3, result.Maximum);

            Assert.AreEqual(2, result.Mean);

        }

Il test ora passerà, ma questo approccio secondo me ha almeno due svantaggi: Se un domani alla classe StatisticValue aggiungiamo una nuova property per esempio Variance dobbiamo ricordarci di andare su tutti i test già scritti per il metodo Calculator e aggiungere una nuova Assert, perchè altrimenti, anche se ci fosse un bug nel codice nel calcolo della varianza, i test passerebbero ugualmente!. Il secondo svantaggio (minore) consiste nell'avere più assert nello stesso test; nel caso in cui fallisse devo andare a vedere dentro il codice del test per capire qual'è il problema, perchè risulta impossibile determinarlo leggendo solo il nome del test che fallisce.

Un'alternativa è quella di ridefinire il comportamento di default del metodo Equals della classe StatisticValues:

        public override bool Equals(object obj)

        {

            if (ReferenceEquals(this, obj)) return true;

            StatisticValues statisticValues = obj as StatisticValues;

            if (statisticValues == null) return false;

            if (m_minimum != statisticValues.m_minimum) return false;

            if (m_maximum != statisticValues.m_maximum) return false;

            if (m_count != statisticValues.m_count) return false;

            if (m_mean != statisticValues.m_mean) return false;

            return true;

        }

In questo modo il test precedente con una sola Assert passerà, rimane ancora il problema che se il test fallisse non posso sapere quale delle varie statistiche non è stata calcolata correttamente; per ovviare a questo possiamo fare l'ovveride del metodo ToString della classe StatisticValues che ci restituisca qualcosa di più significativo.

        public override string ToString()

        {

            return string.Format("Min: {0} Max: {1} Mean: {2} Count:{3}", Minimum, Maximum, Mean, Count);

        }

A volte questa strada potrebbe non essere praticabile perchè ad esempio non ho la possibilità di modificare il codice della classe in quanto si trova in una libreria esterna di terze parti, oppure perchè l'ovveride è già stato fatto in modo diverso: si pensi ad esempio ad una classe Customer con un campo ID dove Equals considera due Customer uguali se hanno lo stesso ID.

La causa di questo problema è data dalla creazione di un'istanza dell'oggetto StatisticValue all'interno del metodo Calculate. Potremmo averne il controllo se la creazione dell'instanza la facciamo fare ad una classe Factory che passiamo alla classe StatisticCalculator.

Questa classe Factory implementerà la seguente interfaccia:

    interface IStatisticValuesFactory

    {

        StatisticValues Create(double minimum, double maximum, double mean, double count);

    }

A questo punto passiamo l'interfaccia al costruttore di StatisticCalculator e modifichiamo il codice del medoto Calculate:

    class StatisticCalculator

    {

        private readonly IStatisticValuesFactory m_factory;

        public StatisticCalculator(IStatisticValuesFactory factory)

        {

            m_factory = factory;

        }

 

        public StatisticValues Calculate(double[] values)

        {

            double min = 0, max = 0, mean = 0, count = 0;

            // ...

            return m_factory.Create(min, max, mean, count);

        }

    }

Nel test dovremmo quindi introdurre un mock di IStatisticValuesFactory:

        [Test]

        public void Test1()

        {

            StatisticValues expectedResult = new StatisticValues();

            expectedResult.Count = 3;

            expectedResult.Minimum = 1;

            expectedResult.Maximum = 3;

            expectedResult.Mean = 2;

 

            MockRepository mocks = new MockRepository();

            IStatisticValuesFactory factory = mocks.CreateMock<IStatisticValuesFactory>();

            Expect.Call(factory.Create(1, 3, 2, 3))

                .Return(expectedResult);

            mocks.ReplayAll();

 

            StatisticCalculator calculator = new StatisticCalculator(factory);

            StatisticValues result = calculator.Calculate(

                new double[] {1, 2, 3});

            Assert.AreEqual(expectedResult, result);

        }

Un'ultima alternativa che mi viene in mente è quella di scrivere un metodo simile all'Assert.AreEqual che usi la reflection per controllare i valori delle property se gli vengono passati delle classi che non siano Value type.

Avete mai avuto un problema simile? Che approccio usate?

Print | posted on venerdì 7 dicembre 2007 13:06 | Filed Under [ Tips ]

Feedback

Gravatar

# re: Alcuni approcci per testare il valore di un oggetto che non sia un value type in uno unit test

Si mi capita abbastanza spesso, ed uso uno degli approcci che hai descritto, cioè faccio l'override del metodo Equals e poi chiamo Assert.AreEqual.
07/12/2007 14:00 | Matteo Baglini
Gravatar

# re: Alcuni approcci per testare il valore di un oggetto che non sia un value type in uno unit test

Personalmente non vado mai ad inserire un metodo equals solo perchè mi serve nel test, basta creare una funzione che tramite reflection faccia il compare di tutte le proprietà di un oggetto ed utilizzare quella. Si scrive con poche righe di codice e la metti in una libreria di helper e buonanotte, personalmente mi piace di più ;) non crei mock ed è veramente semplice da usare.


Alk.
07/12/2007 15:35 | Gian Maria
Gravatar

# re: Alcuni approcci per testare il valore di un oggetto che non sia un value type in uno unit test

+1 per l' override di Equals quando possibile, anche se serve solo per il test "Design for Testability" :-)

Secondo me utilizzare una funzione che via reflection effettua il compare delle proprietà pubbliche non è sempre fattibile, potrebbe capitare che una proprietà non debba influenzare il risultato dell' uguaglianza.

Ad esempio un oggetto ha una prop timestamp che contiene informazioni relative alla data di creazione etc...

Come linea guida : chi modifica la classe deve aggiornare il test relativo al confronto, meglio aggiornare prima il test della modifica ovviamente :-)
07/12/2007 16:02 | Roberto Valenti
Gravatar

# re: Alcuni approcci per testare il valore di un oggetto che non sia un value type in uno unit test

X Luka: quello che hai scritto va bene se non hai molti test a volte succede che non sai esattamente quali parti del sistema vengono impattate da una modifica, personalmente preferisco che per le parti che non si comportano più come prima i test relativi falliscano. Quindi mantenere il design il più possibile su questa linea.

X Gian Maria: La reflection ha dei rischi, se una delle property diventa un oggetto che non è più un value type devi tenerne conto controllato le property dell'oggetto dipendente e poi ci potrebbero essere reference circolari; immagina il caso di due classi master detail dove il master ha un reference al detail e viceversa in questo caso la routine inizierebbe ad essere complessa.
07/12/2007 16:08 | Antonio Ganci
Gravatar

# re: Alcuni approcci per testare il valore di un oggetto che non sia un value type in uno unit test

@antonio. La reflection va usata con attenzione, ho una classe che mi compara e controlla ogni proprietà se è un ienumerable e confronta le collezioni, se una proprietà è a sua volta un altro oggetto la ricontrolla, chiaramente non controllo le reference circolari perchè è complesso, lo so e quindi faccio attenzione quando la uso.

La routine è complessa, ma sinceramente scomodare una factory, mock object e ridefinire il metodo equals solo per comparare le proprietà lo trovo forse un po macchinoso. Considera che se fai override di equals lo devi fare del GetHashCode, e dovresti implementare IEquatable<T> per aderire alle convenzioni del framework........IMHO preferisco fare una singola routine abb complessa (e poi non è cosi complessa, un 100 righe di codice e ti copre il 95% dei casi) tramite reflection, conoscerne i limiti (reference circolari). Cmq questo è un bel terreno su cui discutere, e sarebbe interessante parlarne nel forum di ugi per sentire le opinioni di tutti.

Alk.
07/12/2007 16:23 | Gian Maria
Gravatar

# re: Alcuni approcci per testare il valore di un oggetto che non sia un value type in uno unit test

@Gian Maria : capisco che a volte puo' essere macchinoso fare l' override dell' equals / gethashcode "Iequatable no perchè basta fare riferimento all' equals in override" etc...

Una soluzione che ho adottato e che risolve anche i problemi relativi a proprietà con tipi compositi è stata quella di definire dei custom attribute per marcare le proprietà comparabili e creare un comparatore che via reflection analizza solo queste ultime.

Inoltre se i tuoi oggetti condividono un tipo base, allora puoi fare l' override dell' equals del tipo base e richiamare al sui interno il comparatore in questo modo tutti gli oggetti sottostanti hanno l' equals implementato correttamente, stesso discorso per gethash etc...

Appena ho tempo volevo fare un post a riguardo :-)
07/12/2007 16:47 | Roberto Valenti
Gravatar

# re: Alcuni approcci per testare il valore di un oggetto che non sia un value type in uno unit test

@roberto: La considerazione che alcune proprietà possono non influenzare l'eguaglianza è corretta, il problema è che spesso (almeno personalmente) ho entità con X proprietà, eseguo un operazione su di essa e poi debbo controllare che alcune e non tutte delle X proprietà abbiano dei valori attesi e non solo eguaglianza ma anche "Greater" etc etc. Per ogni tiplogia di test voglio andare a controllare solamente le proprietà che mi interessano. Fare l'override di equals ti permette di definire Uno ed un solo criterio di eguaglianza, invece io se debbo comparare su tutte le proprietà scrivo

Assert.That(obja, MyIs.AllPropertiesEqualTo(objb));

Ma se debbo controllare solamente due proprietà ho

Assert.That(obja, MyIs.SomePropertiesEqualTo(objb, "PropA", "ThirdProperty"));

Cosi facendo ad esempio controllo i due oggetti solamente per le proprietà "PropA" e "ThirdProperty". Ma posso fare test più complessi

Assert.That(obja, MyIs.SomePropertiesEqualTo(objb, "PropA", "ThirdProperty")
.And.Property("PropB").GreaterThan(10));

Cosi confronto i due oggetti solo per le "PropA" e "ThirdProperty" e controllo in and che l'oggetto obja abbia la proprietà "PropB" maggiore di 10.

IMHO Ritengo che il test sia molto leggibile con le interfacce fluenti e sia molto comunicativo.


Alk.
07/12/2007 16:52 | Gian Maria
Gravatar

# re: Alcuni approcci per testare il valore di un oggetto che non sia un value type in uno unit test

Ho aperto una discussione sul forum di UGIALT.NET.
Ecco link:
http://tech.groups.yahoo.com/group/ugialtnet/message/279
07/12/2007 17:35 | Antonio Ganci
Gravatar

# re: Alcuni approcci per testare il valore di un oggetto che non sia un value type in uno unit test

Ok, solo che non ho mai registrato yahoo groups :D :D :D ho provato ma sto aspettando da 5 minuti la mail di verifica attivazione per cui fino a quel momento non posso postare :D, cmq magari si poteva fare direttamente nel forum di UGI o addirittura di GUISA, perchè penso che questa discussione tocchi argomenti architetturali

Alk.
07/12/2007 17:49 | Gian Maria
Gravatar

# re: Alcuni approcci per testare il valore di un oggetto che non sia un value type in uno unit test

X Gian Maria: me l'hai mandato in privato :-) ho già provveduto a postarlo sul group.
07/12/2007 18:24 | Antonio Ganci
Gravatar

# re: Alcuni approcci per testare il valore di un oggetto che non sia un value type in uno unit test

uffaaa lo sapevo che sono stordito oggi :D :D :D, sooooorrryyyyy chiedo venia, ma googlegroups non riesco proprio a digerirlo mi sbaglio sempreeeeeeeee hehe

Alk.
07/12/2007 18:26 | Gian Maria
Gravatar

# Reflection

Reflection
09/12/2007 16:27 | makka
Comments have been closed on this topic.

Powered by:
Powered By Subtext Powered By ASP.NET