Quando ci si avvicina per la prima volta alla pratica dello Unit Test non sempre si riescono a scrivere buoni test. Ricordo che i miei primi esperimenti consistevano in decine di righe di codice per configurare i mock objects e per eseguire diverse assert nello stesso test.
Test scritti in questo modo in genere creano sconforto e conseguente sfiducia nella pratica dello Unit Testing perché non appena è richiesta la modifica di una funzionalità e i test si rompono si perde un tanto tempo per capire cosa testava e perché ora si è rotto.
Considerate il seguente test:
[Test]
public void Constructor_ShouldDoSomeThingOnViewAndModel()
{
IDtoMapper mapper = MockRepository.CreateMock<IDtoMapper>();
IMyView view = MockRepository.CreateMock<IMyView>();
IMyModel model = MockRepository.CreateMock<IMyModel>();
IList<DomainModelData> dummyData = new List<DomainModelData>();
IList<DtoData> dummyDto = new List<DtoData>();
view.Expect(v => v.Save += null).IgnoreArguments();
model.Expect(m => m.GetData()).Return(dummyData);
mapper.Expect(m => m.ToDto(dummyData)).Return(dummyDto);
view.Expect(v => v.SetDataContext(dummyDto);
new MyPresenter(view, model, mapper);
view.VerifyAllExpectation();
model.VerifyAllExpectation();
mapper.VerifyAllExpectation();
}
Puzza vero?
Quali sono i problemi?
Sta testando troppe cose, quindi è poco leggibile e dipende da troppi oggetti . Ciò significa che, tra 2 settimane se lo devo modificare impiego tanto tempo a capire cosa fa e nel caso in cui diventi rosso non riesco ad identificare puntualmente il problema.
La soluzione verso la quale sono andato è un test molto più snello e semplice in cui ho un solo oggetto mockato e verifico una sola cosa.
Questo significa che il test sopra viene spezzato in 4 test differenti:
1) Uno che verifica che il presenter si sottoscrive correttamente all'evento Save della view
2) Uno che verifica che il presenter utilizzi il model per farsi dare i dati da visualizzare
3) Uno che verifica che il presenter chiami il metodo ToDto del mapper per convertire l'oggetto del DM in un DTO da poter passare alla user interface
4) Uno che verifica che il presenter passi alla view il DTO contenente i dati da visualizzare
Sembra uno sforzo inutile, invece:
- I test ottenuti sono decisamente più leggibili
- I test documentano molto meglio le funzionalità dell'applicazione
- Se un test fallisce so esattamente cosa non ha funzionato perché ogni test verifica una singola funzionalità
- Scrivere 4 test al posto di quello sopra non è che sia un grande sforzo ;-)
Il risultato potrebbe essere questo:
[Test]
public void Constructor_ShouldSubscribeToViewEvents()
{
IDtoMapper mapper = MockRepository.CreateStub<IDtoMapper>();
IMyView view = MockRepository.CreateMock<IMyView>();
IMyModel model = MockRepository.CreateStub<IMyModel>();
view.Expect(v => v.Save += null).IgnoreArguments();
MyPresenter presenter = new MyPresenter(view, model, mapper);
view.VerifyAllExpectation();
}
[Test]
public void Constructor_ShouldGetDataFromModel()
{
IDtoMapper mapper = MockRepository.CreateStub<IDtoMapper>();
IMyView view = MockRepository.CreateStub<IMyView>();
IMyModel model = MockRepository.CreateMock<IMyModel>();
IList<DomainModelData> dummyData = new List<DomainModelData>();
model.Expect(m => m.GetData()).Return(dummyData);
MyPresenter presenter = new MyPresenter(view, model, mapper);
model.VerifyAllExpectation();
}
[Test]
public void Constructor_ShouldGetDtoUsingMapper()
{
IDtoMapper mapper = MockRepository.CreateMock<IDtoMapper>();
IMyView view = MockRepository.CreateStub<IMyView>();
IMyModel model = MockRepository.CreateStub<IMyModel>();
IList<DomainModelData> dummyData = new List<DomainModelData>();
IList<DtoData> dummyDto = new List<DtoData>();
mapper.Expect(m => m.ToDto(dummyData)).Return(dummyDto);
MyPresenter presenter = new MyPresenter(view, model, mapper);
mapper.VerifyAllExpectation();
}
[Test]
public void Constructor_ShouldSetDataContextOnView()
{
IDtoMapper mapper = MockRepository.CreateStub<IDtoMapper>();
IMyView view = MockRepository.CreateMock<IMyView>();
IMyModel model = MockRepository.CreateStub<IMyModel>();
IList<DtoData> dummyDto = new List<DtoData>();
view.Expect(v => v.SetDataContext(dummyDto);
MyPresenter presenter = new MyPresenter(view, model, mapper);
view.VerifyAllExpectation();
}