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?