Premetto che capisco l'importanza di test, quando ancora il test strutturato era qualcosa di sconosciuto quasi ogni mia classe java aveva un metodo main per testare la classe stessa.
Il Testing strutturato è - da qualche anno - entrato a fare parte del mondo .NET, prima con strumenti di terze parti NUnit - giunto direttamente dal lato oscuro, vedi JUnit - poi e ora con strumenti di testing integrati direttamente in Visual Studio. Senza discuisire su quelle strumento sia migliore per il testing, credo che l'aver creato una "standardizzazione" del testing sia un grande passo avanti nell'ottica di creare codice qualitativamente buono e facilmente mantenibile. Tuttavia, nonostante ho infinito apprezzamento verso le varie disclipine di testing - che sicuramente richiede un certo rigore da parte di chi sviluppa - non si può negare che tale disciplina aumenta - anche notevolmente - i tempi di sviluppo.
Con l'introduzione del testing strutturato ecco la nascita di nuove fobie e nuove interessanti parole con cui ci si riempie la bocca (esempio Code Coverage). Il Code Coverage NON misura la qualità del codice ma la quantità di codice coperto dal testing... che NON è la stessa cosa. Ed in effetti nessuno ha mai detto il contrario nel definirlo: "Code coverage is a measure used in software testing. It describes the degree to which the source code of a program has been tested." (cit. Wiki: Code Coverage)
Seguite le seguenti prove.
Class 1
public class Class1
{
public int Divide(int a, int b)
{
return a / b;
}
}
|
[TestMethod()]
public void DivideTest()
{
Class1 target = new Class1();
int a = 1;
int b = 1;
int expected = 1;
int actual;
actual = target.Divide(a, b);
Assert.AreEqual(expected, actual);
}
[TestMethod()]
public void Class1ConstructorTest()
{
Class1 target = new Class1();
Assert.IsNotNull(target);
}
|
Ho Code Coverage 100%, ma se venisse richiesta una divisione per 0 sarebbe sollevata l'eccezione "DivideByZeroException".
Class 2
public class Class2
{
private Object[] items = new Object[] { "Class2_Item0" };
public Object Get(int index)
{
return items[index];
}
}
|
[TestMethod()]
public void Class2ConstructorTest()
{
Class2 target = new Class2();
Assert.IsNotNull(target);
}
[TestMethod()]
public void GetTest()
{
Class2 target = new Class2();
int index = 0;
object expected = "Class2_Item0";
object actual;
actual = target.Get(index);
Assert.AreEqual(expected, actual);
}
|
Ho Code Coverage 100%, ma se venisse richiesto un indice non presente sarebbe sollevata l'eccezione "ArgumentOutOfRangeException".
Class 3
public class Class3
{
private Object[] items = new Object[] { "Class3_Item0" };
public object Get(int index)
{
if (index >= items.Length)
{
return null;
}
else
{
return items[index];
}
}
|
[TestMethod()]
public void Class3ConstructorTest()
{
Class3 target = new Class3();
Assert.IsNotNull(target);
}
[TestMethod()]
public void GetTest()
{
Class3 target = new Class3();
int index = 0;
object expected = "Class3_Item0";
object actual;
actual = target.Get(index);
Assert.AreEqual(expected, actual);
}
|
Il codice di Class3 è sicuramente da ritenere "migliore" ma avrò un Code Coverage 83,33% in quanto effettivamente il test copre solo l'istruzione nello scope dell'else. beh non è certo difficile avere il 100% ma non è questo il mio scopo.
Da queste semplici prove ne ricavo che il code coverage è importante per chi sviluppa in quanto ha la sicurezza che il codice è coperto dai test... ma non è garanzia per nessuno che il codice sia qualitativamente buono e privo di bug. Sarebbe opportuno che - come in java - i metodi dichiarassero le eccezioni sollevate? Sarebbe oppotuno che il calcolo del code coverage sapesse analizzare in autonomia il codice e le eccezioni sollevabilili? Sinceramente non ho un'idea chiara - al momento - di quello che sarebbe opportuno. Ma per quanto detto leggere un buon code coverage - sto parlando quello calcolato da VS - mi lascia il tempo che trova.
Ora che ho dato una dimensione al code coverage, voglio continuare con le considerazioni... seguite questo esempio.
Class 4
public class Class4
{
public String GetValue()
{
string value = Facade.Get();
return value;
}
class Facade
{
public static IProvider provider;
private static IProvider Provider
{
get
{
if (provider == null)
{
//Qui si potrebbe leggere provider da configurazione o usare qualche application block per inversion of control.
Type type = typeof(ProviderImpl);
provider = (IProvider) Activator.CreateInstance(type);
}
return provider;
}
}
public static String Get()
{
return Provider.Get();
}
}
interface IProvider
{
String Get();
}
class ProviderImpl : IProvider
{
#region IProvider Members
public String Get()
{
return "Esempio";
}
#endregion
}
}
|
[TestMethod()]
public void GetValueTest()
{
Class4 target = new Class4();
string expected = "Esempio";
string actual;
actual = target.GetValue();
Assert.AreEqual(expected, actual);
}
[TestMethod()]
public void Class4ConstructorTest()
{
Class4 target = new Class4();
Assert.IsNotNull(target);
}
|
Ho Code Coverage 100% senza necessità di testare ogni singola componente nello specifico... senza Fake o Mock. Come dicevo il Code Coverage lascia il tempo che trova anche se poi effettivamente il codice del mini-sistema è completamente coperto. Qual'è la vera esigenza di testare ogni singola componente/layer? Una attenta analisi di evenuali errori nell'esecuzione dei test non possono compensare i test capillari? Con "attenta analisi" intendo tenere in considerazione lo stack trace dell'errore in caso di runtime oppure Assert intelligenti per la valutazione di errori funzionali. Cosa si possono prevenire con i test capillari delle singole componenti? Errori funzionali dei singoli layer che se isolati evitano perdite di tempo e sessioni di debug per identificare la causa del malfunzionamento. In genere tendo a preferire i test verticali di tutto l'impianto integrato fatta eccezione per componenti molto particolari per le quali giudico opportuno testare isolatamente.
Quindi che ne deduco? I test capilari solo se davvero ne vale la pena... in gerere 1 ora di debug credo che sia meglio di 4 ore di implemetazione dei test.
Il mantenimento dopo un refactoring? Se il code coverage è alto ci sono buone possibilità di scovare le regressioni dopo un refactoring in componenti di layer comune a più parti nella stessa misura di un test capillare; se la componente rifattorizzata non dispone di test specifici per la stessa vorrà dire che probabilemente dovrò spendere un pò più di tempo per scovare la vera origine dell'errore... ma in fondo a volte la rifattorizzazione di componenti significa anche il cambio funzionale di alcuni suoi comportamenti, per cui quanto dovrei anche spendere per risistemare tutti i TestCase relativi?
oO0 (... io questa moda del test di ogni cosa ad ogni costo non so se la approvo ...)
posted @ giovedì 5 giugno 2008 00:59