Per comprendere meglio i concetti esposti in questo post consiglio prima la visione del seguente webcast, la lettura degli articoli che avevo segnalato nel precente post ed infine consiglio l'ascolto dell'ottimo webcast di Corrado Cavalli scaricabile da guisa.
Per prima cosa vediamo uno screen shot della user interface che vogliamo sviluppare in TDD:
La form precedente serve per selezionare un titolo del mercato azionario (nel dominio utente chiamato symbol), permettendo di scegliere se aggiungerlo ad uno dei chart correntemente visualizzati o crearne uno nuovo. In una finestra si possono visualizzare più chart ognuno contenente uno o più titoli.
A questo punto entrano in gioco tre attori: il model, il presenter e la view. Iniziamo vedendo lo schema delle dipendenze tra le tre classi:
Il presenter contiene un riferimento al model e alla view, mentre il model e la view comunicano con il presenter tramite eventi.
La view è la nostra form ed in generale la user interface, il model usa gli oggetti di business o di dominio e non sa nulla di user interface, mentre il presenter è la colla tra i due mondi cioè trasforma le informazioni degli oggetti di dominio in una forma visualizzabile facilmente dalla user interface.
Iniziamo a sviluppare in TDD il presenter; per poterlo testare agevolmente facciamo in modo che dipendenda da due interfacce una che verrà implementata dal model e l'altra dalla view.
Per non scrivermi a mano degli stub userò il framework Rhino Mocks (maggiori informazioni nel post di papo e nel mio precedente).
Il primo test che scriveremo riguarda il riempimento della combobox con i nomi dei symboli. Questa informazione verrà fornita dal model tramite un evento, vediamo il sequence diagram:
In pratica quando nel model viene chiamato il metodo SetSymbolNameList viene segnalato l'evento SymbolNameListChanged a cui il presenter è sottoscritto; e nell'event handler viene chiamata la vista in modo che venga riempita la combobox.
Iniziamo ad impostare la classe di test SelectSymbolPresenterTests:
using MbUnit.Framework;
using Rhino.Mocks;
using Rhino.Mocks.Interfaces;
namespace PresenterFirstExample
{
[TestFixture]
public class SelectSymbolPresenterTests
{
private MockRepository _mocks;
private ISelectSymbolModel _model;
private ISelectSymbolView _view;
[SetUp]
public void SetUp()
{
_mocks = new MockRepository();
_model = _mocks.CreateMock<ISelectSymbolModel>();
_view = _mocks.CreateMock<ISelectSymbolView>();
}
[TearDown]
public void TearDown()
{
_mocks.VerifyAll();
}
[Test]
public void WhenTheModelFiresTheSymbolListChangedThePresenterCallsTheViewToFillTheList()
{
}
}
}
Il framework usato è MbUnit, ma la conversione ad altri framework (NUnit, Microsoft Quality Tools) non è un grosso problema.
Il presenter dipende da due interfacce (ISelectSymbolModel e ISelectSymbolView) quindi nel momento in cui scriviamo il test non esiste ancora la form e potrebbe anche non esistere ancora il business layer.
Questo porta dei grossi vantaggi perchè progettando il sistema come se questi due attori non esistessero rende semplice la loro modifica o sostituzione, es: se anzichè una form la view dovesse cambiare in una pagina web l'unica cosa da cambiare è l'implementazione concreta dell'interfaccia ISelectSymbolView, stesso discorso se anzichè una combobox dovessimo usare un DataGrid.
Il nome del test è un pò lungo ma è importante dargli un nome che indichi chiaramente cosa vogliamo testare piuttosto che quello che fa il test. Mi spiego meglio: se il test che abbiamo scritto fallisce leggendo il nome del test si deve essere in grado di capire senza guardare il codice al suo interno cosa del nostro sistema non funziona più correttamente.
La prima cosa che vogliamo testare è che il presenter si sottoscriva all'evento del model, per ottenere questo risultato aggiungiamo al metodo SetUp il seguente codice:
_model.SymbolNameListChanged += null;
_symbolNameListChangedRaiser = LastCall.Repeat.Once().IgnoreArguments().GetEventRaiser();
Le due righe di codice precedenti possono sembrare un pò strane, ma è il modo con cui si controlla con rhino mocks se ci si è sottoscritti ad un evento; la variabile _symbolNameListChangedRaiser è di tipo IEventRaiser e verrà usata nel metodo di test per simulare l'evento.
Per far compilare il codice precedente creiamo il model:
using System;
namespace PresenterFirstExample
{
public interface ISelectSymbolModel
{
event EventHandler<StringListEventArgs> SymbolNameListChanged;
}
}
La classe StringListEventArgs:
using System;
namespace PresenterFirstExample
{
public class StringListEventArgs : EventArgs
{
private readonly string[] _strings;
public StringListEventArgs(string[] _strings)
{
this._strings = _strings;
}
public string[] StringList { get { return _strings; } }
}
}
A questo punto finalmente scriviamo il nostro test:
[Test]
public void WhenTheModelFiresTheSymbolListChangedThePresenterCallsTheViewToFillTheList()
{
string[] symbolNames = new string[] {"ENEL-MIL", "ENI-MIL", "G-MIL"};
_view.FillSymbolNameList(symbolNames);
LastCall.Repeat.Once();
_mocks.ReplayAll();
new SelectSymbolPresenter(_model, _view);
_symbolNameListChangedRaiser.Raise(_model, new StringListEventArgs(symbolNames));
}
Per chi non è abituato ad usare Rhino Mocks può sembrare un pò ostico, ma è più semplice di quello che sembra. La prima riga crea la variabile symbolNames che contiene l'elenco dei symbol che arriveranno come dati dall'evento (vedi l'ultima riga del test), le due righe successive significano che ci aspettiamo che il presenter chiami la vista passando come argomento l'elenco dei nomi creato prima. La chiamata ReplayAll indica a Rhino Mocks che da quel punto in avanti si è in Play Mode cioè che abbiamo finito il codice in cui dichiaramo che cosa ci aspettiamo che venga chiamato dei nostri oggetti mock.
Se lanciamo adesso il test vediamo che non passa per il seguente motivo:
TestCase 'SelectSymbolPresenterTests.SetUp.WhenTheModelFiresTheSymbolListChangedThePresenterCallsTheViewToFillTheList.TearDown' failed: ISelectSymbolModel.add_SymbolNameListChanged(any); Expected #1, Actual #0.
Per far passare il test scriviamo la classe presenter come segue:
using System;
namespace PresenterFirstExample
{
public class SelectSymbolPresenter
{
private readonly ISelectSymbolModel _model;
private readonly ISelectSymbolView _view;
public SelectSymbolPresenter(ISelectSymbolModel model, ISelectSymbolView view)
{
_model = model;
_view = view;
_model.SymbolNameListChanged += new EventHandler<StringListEventArgs>(Model_SymbolNameListChanged);
}
void Model_SymbolNameListChanged(object sender, StringListEventArgs e)
{
_view.FillSymbolNameList(e.StringList);
}
}
}
A questo punto possiamo iniare a scrivere altre funzionalità.
Continua...