In questo articolo vedremo come effettuare un test del nostro codice sorgente utilizzando Unit Test. Per chi non ne fosse a conoscenza i test sono alla base di un modello di sviluppo chiamato, appunto, test-driven development e ne sono tanto alla base, che le procedure di test vengono scritte addirittura prima del codice da testare.
Non chiedetemi se sono favorevole a questo approccio; i miei brevi studi sulla calcolabilità e decidibilità mi fanno inorridire (e avrebbero fatto inorridire anche A.Turing (link) all’idea di dimostrare il corretto funzionamento di un programma tramite un altro programma, ma se ridimensioniamo le aspettative e diciamo che i test non servono a dimostrare che un programma funzioni ma semplicemente per verificare che l’output prodotto a parità di input segua le nostre aspettative.. beh allora direi che tutto si fa più interessante.
Per chi non avesse difficoltà con l’inglese consiglio una lettura a questo articolo: è l’articolo dal quale ho preso spunto per scrivere questo testo che vi accingete a leggere. Tengo a precisare che la mia non vuole essere una semplice traduzione dell’articolo originale, ma reputo anche giusto sottolineare che non è tutta farina del mio sacco e una citazione alla fonte sicuramente male non farà.
Prima di addentrarci nel nostro articolo vediamo brevemente il significato di alcuni termini:
- test case: Un singolo test usato per verificare una particolare funzionalità software.
- test suite: Un insieme di test case che verificano il comportamento di una porzione più ampia di codice.
- logic tests: Sono i test che non vengono eseguiti all’interno di un’applicazione vera e propria. Si possono scrivere logic test e far sì che vengano eseguiti in fase di compilazione. Questo assicura al test una certa indipendenza dall’applicazione finale
- application tests: Per convesso sono i test che vengono eseguiti all’interno di un’applicazione realmente funzionante. Sono eseguiti quindi a runtime non a compile-time. (non verranno presi in esame in questo articolo)
Come aggiungere un logic test al nostro software?
Xcode 4 prevede già la possibilità di aggiungere, in fase di creazione del progetto, tutto quanto necessario per creare un unit test, ma per assicurarci una maggiore comprensione partiremo da un progetto vuoto.
Creiamo quindi un nuovo progetto, io l’ho chiamato “TestOC”, perché il framework per effettuare i test si chiama “OCUnit”. Ok, ok… l’idea di inserire la parola “test” nel nome del progetto non è stata proprio felicissima 🙂
Creato il progetto bisogna aggiungere un nuovo unit-test bundle target e per farlo basta selezionare dentro Xcode l’icona del progetto e cliccare sul pulsante “Add target” presente sulla destra. Selezioniamo quindi “Cocoa Touch Unit Testing Bundle” dalla schermata che appare e diamogli un nome significativo.


A questo punto dovreste trovarvi con una struttura di file simile a questa:

dove si può notare che xCode ha già creato per noi sia i file necessari per la compilazione, sia una classe “TestOCLogicTest” dove poter scrivere i nostri test.
Se la vostra versione di xCode non dovesse aggiungere questa classe basterà aggiungerla manualmente tramite la solita procedura “file -> new->new file” selezionando il tipo “Objective-c Test case Class” e facendo attenzione di spuntare la casella solo per il target di testing.

Se xcode ha creato per voi la classe “TestOCLogicTest” avrà inserito all’interno una serie di metodi standard, per il momento commentateli ed aggiungete nel file .m questo codice:
- (void) testFail {
STFail(@"Must fail to succeed.");
}
Selezionate lo schema appropriato usando il simulatore come device e cliccare sul pulsante “Test”, vedrete la vostra compilazione fallire miseramente con questo messaggio:

Cosa è successo? Xcode ha effettuato il test del nostro codice, nello specifico ha eseguito il metodo “testFail” della classe “TestOCLogicTest” che ha come unico scopo quello di far fallire il test.
Adesso facciamo un’altra prova e sostituiamo il metodo che abbiamo appena scritto con questo:
- (void) testPass {
STAssertTrue(TRUE, @"");
}
Cosa succederà eseguendo il test? Apparentemente nulla, xcode dirà “compilazione andata a buon fine” senza ulteriori dettagli, però se andiamo a guardare i messaggi di log nella console vedremo una serie di informazioni molto utili.

Se proprio siete curiosi potete provare ad inserire entrambi i metodi nella classe, noterete che vengono eseguiti tutti e due, indipendentemente dal loro ordine e sulla console viene mostrato un report di cosa è successo:

Scriviamo i nostri test
Ma cos’è in soldoni un test? È un metodo di istanza di una classe “SenTestCase” che non ha parametri, il suo nome inizia per test… e non ha nessun valore di ritorno. All’interno di questi metodi andremo and inizializzare i nostri oggetti e verificheremo se tutto è andato a buon fine usando le Unit-Test macro. Chiaro, no?
Un caso concreto
Giusto per chi non avesse afferrato tutto tutto… descriviamo una situazione tipica:
Abbiamo creato una nostra classe (MyClass) che viene inizializzata prelevando alcuni valori da un file xml, ad esempio con un metodo simile a questo:
-(id)initWithContentOfFile:(NSString *)filename {
NSBundle *thisBundle = [NSBundle bundleForClass:[self class]];
NSString *filePath = [thisBundle pathForResource:filename ofType:@"plist"];
NSString *filePath = [[NSBundle mainBundle] pathForResource:filename ofType:@"plist"];
NSDictionary *dict = [[NSDictionary alloc] initWithContentsOfFile:filePath];
if ([dict objectForKey:@"Name"]) {
self = [super init];
if (self) {
self.name = [dict objectForKey:@"Name"]
}
}
else {
self = nil;
}
return self;
}
Benissimo, in questo modo siamo sicuri che la nostra classe sarà consistente con l’idea che abbiamo in testa.
Facciamo adesso un balzo in avanti di qualche mese, avete assolutamente dimenticato questo aspetto un pò particolare di questa classe e, visto che sono cambiate le specifiche, tra le altre 100 modiche fatte al codice qualcuno ha anche deciso di cambiare nel file plist la chiave che da “Name” è diventata “Nome”… e ora stranamente il programma non funziona più…suona familiare? Quanto debug sarà necessario prima di capire che la classe “MyClass” non può essere istanziata con quel file plist e quindi risulta essere nil?
Risolviamo il nostro problema con unit test
Il programma non funziona perché sono venute meno le precondizioni che avevamo stabilito? Ma questo è un lavoro per unit test! Quando abbiamo scritto per la prima volta il nostro metodo “initWithContentOfFile” avremmo potuto scrivere un test case per questo metodo, così adesso che sono cambiate le precondizioni sicuramente il test ci avrebbe avvertito! Ma come creiamo quindi il test? Potremmo scrivere un test simile a questo:
-(void)testCreationMyClassIstance {
NSString *filename = @"preferences";
MyClass *c = [[MyClass alloc] initWithContentOfFile:filename];
STAssertNotNil(c, @"MyClass non è stata inizializzata");
}
nel quale creiamo una istanza della classe “MyClass” e poi usiamo la macro “STAssertNotNil” per verificare che non sia nil.
Se questo test viene eseguito prima di fare la modifica al file plist vediamo che l’esito è positivo:
mentre se invece effettuiamo maldestramente una modifica al file plist otteniamo immediatamente un comodissimo messaggio d’errore
Alla prossima!
IgnazioC










2 Responses to “Uno sguardo a Unit test: impariamo a prevenire ore e ore di debug durante lo sviluppo di Applicazioni iOS”
8 Agosto 2011
ignaziocandateci piano con le critiche…sono nuovo dell’argomento…
12 Agosto 2011
Luca DegasperiUn passo alla volta, è importante dire che se si vuole seguire la filosofia TDD i test non sono più solo una comodità ma un obbligo. E quindi non devono essere visti come uno spreco di tempo, o una cosa giusto per fare contento il capo / cliente. O si fanno bene o non si fanno! Inoltre sempre secondo le specifiche TDD vanno scritti PRIMA i test e DOPO il codice che dovrà superare i test. Questo non solo aiuta a fare chiarezza nel codice ma anche all’interno della mente di chi programma e del gruppo di lavoro di qui si fa parte.