Nella scrittura di unit test una delle cose più importanti è l'assoluta indipendenza dei test, un test non deve essere influenzato dal risultato di altri test. Particolarmente importante è quindi l'esecuzione di una serie di operazioni, che vanno sotto il nome di fixture teardown, necessarie per riportare lo stato di ogni parte di una shared fixture allo stato iniziale. Si consideri questo banale esempio
Queste due banali classi rappresentano il concetto generico di classi che hanno internamente una shared fixture. In un caso reale chiamando il metodo AddSomething della classe SomeClass si modificano alcune parti del sistema per cui è necessario chiamare RemoveSomething() per riportare l'ambiente allo stato iniziale. Stessa cosa vale per la SomeDisposable che in più implementa anche IDisposable. Naturalmente nell'esempio le classi fanno solamente un dump su console. Se un SUT ha necessità di una fixture gestita da questi oggetti un primo approccio potrebbe essere il seguente.
Naturalmente questo test ha uno smell: se avviene una qualsiasi eccezione durante l'esercizio del sut non viene eseguita la parte finale di teardown e quindi i successivi test potrebbero essere influenzati fa una fixture non completamente cancellata. Il nunit ha la possibilità di utilizzare gli attributi [SetUp] e [TearDown], in alternativa basta utilizzare un try..finally, ma il problema è un altro. Il codice di teardown tende a divenire complesso, cosa succede ad esempio se l'eccezione viene lanciata dal metodo test2.DoSomething(100)? In questo caso il metodo fallisce e non deve quindi essere chiamato il corrispondente test2.UnDoSomething(), altrimenti anche in questo caso non c'è un match tra il numero di Do e di Undo. Questa situazione è problematica perché è difficile dire dal codice di teardown se qualche metodo è stato o meno chiamato. Inoltre vi è un'ulteriore complicazione, cosa succede se test3.UndoSomething() lancia un eccezione? Anche in questo caso la procedura di teardown si interrompe e non verranno mai chiamati i vari test2.UndoSomething etc.
Una prima soluzione è creare una classe che fa il management di una fixture. (Il tipo Proc è semplicemente un delegate di una funzione che non accetta parametri e torna void)
Questa classe tiene traccia in un dizionario di tutte i passi necessari per la fixture, memorizzando per ogniuno l'operazione di setup e quella corrispondente di teardown (prima evidenziazione), durante l'esecuzione del setup ogni passo viene eseguito singolarmente in una clausola try..catch, se nessuna eccezione viene generata, il corrispondente passo di teardown viene aggiunto ad uno stack (lista lifo) che mi assicura che i passi di teardown verranno eseguiti nell'ordine inverso a quelli di setup. Nel teardown invece si effettua il pop di tutti gli elementi dello stack invocando la corrispondente funzione di teardown, anche in questo caso dentro un blocco try..catch in modo che se una operazione genera errore le successive verranno comunque eseguite.
Come si può vedere tutta la parte di inizializzazione viene racchiusa in un'unica funzione che fa uso di anonymous delegate per creare gli step della fixture. Come si può notare per ogni parte di setup viene anche inserita una corrispondente funzione di teardown. Il codice invece utilizza un semplice try..finally per essere sicuro che il metodo fixture.ExectuteTearDown() venga eseguito correttamente. Modificando il metodo DoSomething della classe SomeDisposable e forzando un eccezione quando il valore del parametro è minore di 100 si ha questo output.
SomeClass Init
SomeClass AddSomething
SomeDisposable DoSomething
SomeDisposable UndoSomething
SomeDisposable Dispose
SomeClass RemoveSomething
Che mostra chiaramente come il test esegue automaticamente il dispose degli elementi che erano stati precedentemente costruiti. In realtà questa soluzione è molto grezza e potrebbe essere rifinita aggiungendo altre funzionalità, ma è una buonissima base da cui partire.
Alk.