cavolo! non so quanto tempo è che mi dico "devi iniziare
una serie di post di mock objects" e non trovo mai il tempo per organizzare le
idee e partire... ora, dopo l'ennesima volta che Ayende mi stupisce,
ho deciso di buttare giù la bozza di un primo post/articoletto su la mia
libreria di mock preferita: Rhino.Mocks.
per paura di dilungarmi troppo (so di essere prolisso e
"logorroico"), vado subito al cuore della discussione. mi interessa mostrare un
sistema che ho adottato per descrivere il design delle mie applicazioni (per
intederci, io come molti considero il TestDriven Development e in
particalare l'uso dei Mock Object uno strumento di progettazione più che di
testing).
mi succede spesso di progettare infatti, sequendo la mia attitutidine
prettamente "top-down" (nonostante uno dei miei autori preferiti suggerisca "Code from the Bottom Up"), partendo dal metodo più esterno (un qualche Application
Controller o Façade
) e via via suddividere il compito in sotto responsabilità
più semplici. assegnare le singole sotto responsabilità è la parte più critica,
e le alternative di progetto che conosco e uso sono:
- delega:
estrarre delle interfacce "collaboratrici", cosa che faccio spesso: in questo
modo ottengo tanti oggetti con poche responsabilità (il principio "favorire la
composizione di oggetti piuttosto che l'ereditarietà di classe")
- ereditarietà:
definire una classe (di solito astratta) in cui pongo i singoli
sotto-metodi come astratti o virtuali e lascio alle classi concrete il compito
di implementarli (in pratica il pattern Template Method,
che invece "favorisce l'ereditarietà di classe" !)
a questo punto, per descrivere in un unit test la
struttura risultante ho varie possibilità. per quanto riguarda la delega. la
soluzione più comune (e valida) è quella di usare il principio
dell'Inversione del Controllo, fornendo
un qualche meccanismo per sostituire a tempo di esecuzione (del test) le istanze
delle varie interfacce con dei mock objects: ad esempio con costruttori che li
accettano come parametri o usando delle proprietà con "setter". fin qui nulla di
nuovo (per me, mentre per approfondire consiglio la serie di post di J.Miller sul TDD Starter Kit). eccovi un
esempio per il test, si tratta di un "handler" che per salvare un oggetto
del dominio utilizza un service layer per la validazione e poi un data access per
le operazioni di persistenza:
using System;
using NUnit.Framework;
using Rhino.Mocks;
[TestFixture]
public class PartialMocksFixture
{
private MockRepository _mocks;
[SetUp]
public void SetUp()
{
_mocks = new MockRepository();
}
[TearDown]
public void TearDown()
{
_mocks.VerifyAll();
}
[Test]
public void WithComposition()
{
Handler fixture;
IServiceLayer mockService;
IDataAccess mockDataAccess;
DomainObject item;
//init
mockService = _mocks.CreateMock<IServiceLayer>();
mockDataAccess = _mocks.CreateMock<IDataAccess>();
item = new DomainObject();
fixture = new Handler(mockService, mockDataAccess);
//expect
using(_mocks.Ordered())
{
Expect
.Call(mockService.Validate(item))
.Return(true);
mockDataAccess.SaveOrUpdate(item);
LastCall
.IgnoreArguments();
}
_mocks.ReplayAll();
//execute
fixture.Save(item);
}
}
ed ecco il (banale) codice in grado di compilare ed eseguire con successo il
test:
public interface IServiceLayer
{
bool Validate(DomainObject item);
}
public interface IDataAccess
{
void SaveOrUpdate(DomainObject item);
}
public class DomainObject{ }
public class Handler
{
private IServiceLayer _serviceLayer;
private IDataAccess _dataAccess;
public Handler(IServiceLayer serviceLayer, IDataAccess dataAccess)
{
_serviceLayer = serviceLayer;
_dataAccess = dataAccess;
}
public virtual void Save(DomainObject item)
{
if( _serviceLayer.Validate(item) )
{
_dataAccess.SaveOrUpdate(item);
}
}
}
veniamo ora al caso in cui, per scelte di progetto, si
sia scelto di usare l'ereditarietà: questo significa che l'oggetto da
testare è lo stesso che contiene i metodi "fake" (quelli cioè da sostituire). si
tratta della situazione in cui dobbiamo scrivere i test ad esempio di un
Layer SuperType. come procedere quindi alla
scrittura del test di unità?
parto con la soluzione che ho usato per prima, che è
durata per qualche tempo e che oggi ho deciso di abbandonare. si tratta (come
dice il titolo del post) di usare i delegates
: per ogni metodo da sostituire definisco un delegate (con firma
corrispondente), e introduco nella classe base (astratta o meno) una proprietà
pubblica che "punti" all'implementazione del metodo: durante il test sostituiremo
i delegates con dei mock object, mentre nel codice reale ci saranno dei
delegates ai metodi reali della classe stessa. ecco il codice del test:
[Test]
public void WithDelegates()
{
BaseHandler fixture;
ValidatorDelegate mockValidator;
PersisterDelegate mockPersister;
DomainObject item;
//init
mockValidator = _mocks.CreateMock<ValidatorDelegate>();
mockPersister = _mocks.CreateMock<PersisterDelegate>();
item = new DomainObject();
fixture = new BaseHandler();
fixture.Validator = mockValidator;
fixture.Persister = mockPersister;
//expect
using(_mocks.Ordered())
{
Expect
.Call(mockValidator(item))
.Return(true);
mockPersister(item);
LastCall
.IgnoreArguments();
}
_mocks.ReplayAll();
//execute
fixture.Save(item);
}
ed ecco il codice per eseguirlo con successo:
public delegate bool ValidatorDelegate(DomainObject item);
public delegate void PersisterDelegate(DomainObject item);
public class BaseHandler
{
private ValidatorDelegate _validator;
private PersisterDelegate _persister;
public ValidatorDelegate Validator { set { _validator = value; } }
public PersisterDelegate Persister { set { _persister = value; } }
public BaseHandler()
{
_validator = new ValidatorDelegate(this.Validate);
_persister = new PersisterDelegate(this.SaveOrUpdate);
}
public virtual bool Validate(DomainObject item) { throw new NotImplementedException(); }
public virtual void SaveOrUpdate(DomainObject item) { throw new NotImplementedException(); }
public virtual void Save(DomainObject item)
{
if( _validator(item) )
{
_persister(item);
}
}
}
come è facile intuire, questa soluzione è piuttosto "noiosa", perchè ci
obbliga a dichiarare una classe delegate per ogni "passo" del metodo principale
e inoltre dobbiamo anche ricordarci sempre di impostare i delegate sui metodo
corretti. insomma, se troviamo qualcosa di meglio non mi dispiace. (per
intenderci, io adoro i delegates, ma qui mi sembrano un po' un abuso).
l'ultima soluzione (per me definitiva fino a prova
contraria) ce la fornisce l'ormai sempre più sorprendente Ayende, con la sua
Rhino.Mocks. quindi, intendiamoci bene, questa soluzione
funziona a meraviglia con questa libreria di mock, ma non ho idea se
esista o meno qualcosa di analogo nelle altre (da tempo ho smesso di
aggiornarmi). ecco di cosa si tratta: usiamo come oggetto di test un oggetto
mock, definito come "mock parziale". il
comportamento infatti dei metodi di un mock parziale è
- quello reale, di default
- quello mock, in caso si definisca una
expectation su tale metodo
ecco come diventa il test di unità:
[Test]
public void WithPartialMocks()
{
AnotherHandler fixture;
DomainObject item;
//init
item = new DomainObject();
fixture = (AnotherHandler)_mocks.PartialMock(typeof(AnotherHandler));
//expect
using(_mocks.Ordered())
{
Expect
.Call(fixture.Validate(item))
.Return(true);
fixture.SaveOrUpdate(item);
LastCall
.IgnoreArguments();
}
_mocks.ReplayAll();
//execute
fixture.Save(item);
}
ed ecco il codice per farlo eseguire con successo:
public class AnotherHandler
{
public virtual bool Validate(DomainObject item) { throw new NotImplementedException(); }
public virtual void SaveOrUpdate(DomainObject item) { throw new NotImplementedException(); }
public virtual void Save(DomainObject item)
{
if( Validate(item) )
{
SaveOrUpdate(item);
}
}
}
come si vede questa soluzione è più compatta, sia nel
test che nell'implementazione (per l'assenza dei campi privati, di proprietà
setter o di costruttori). e inoltre vale la pena notare che funziona (come in questo caso) anche con classi concrete e non
necessariamente astratte, basta utilizzare metodi pubblici virtuali (insomma, le stesse limitazioni che
si hanno per i mock di classi invece che di interfacce). per le
mie esigenze e le mie abitudini di progettazione è un'ottimo risultato, spero
possa tornare utile anche a qualcun'altro.
Alla prossima!
-papo-