Uno dei fattori che ci portano a scegliere MVC rispetto a Web Forms è la presenza di requisiti di manutenibilità, legati probabilmente a un lungo ciclo di vita dell'applicazione, che spesso si traducono in un requisito di testabilità. Infatti durante la creazione di un progetto MVC la prima cosa che ci viene chiesta dal template è se vogliamo creare anche un progetto di test associato.
La netta separazione tra view e controller fornita da questo framework ci consente di sostituire dei test unitari alle view invocando direttamente le action e verificare il risultato dell'elaborazione. Purtroppo non si può dire la stessa cosa del legame tra (view)model e controller dove spesso un'istanza di una classe che offre i servizi del model è creata direttamente durante l'inizializzazione della classe controller o, in caso di più servizi, direttamente nei metodi che rappresentano le nostre action.
Questo è un problema: affinché un test automatico sia unitario (unit test) e ripetibile è importante poter isolare i metodi testati in modo che eventuali invocazioni ad altri metodi, interne al metodo stesso, non influenzino il risultato.
Per ottenere questo risultato ci viene incontro un principio di ingegneria del software chiamato Dependency Inversion Principle, o DIP, il quale ci dice:
- Moduli di alto livello non devono dipendere da moduli di basso livello
- Le astrazioni non devono dipendere dai dettagli, piuttosto i dettagli devono dipendere dalle astrazioni
- Le dipendenze dirette vanno eliminate inserendo un meccanismo di protezione che isola l'implementazione dal suo utilizzatore
Rispettare questo principio con linguaggi di programmazione orientati agli oggetti è abbastanza banale, stiamo praticamente parlando del legame tra un'interfaccia e la sua implementazione (Figura 9), non altrettanto banale potrebbe risultare invece il come istanziamo l'implementazione di un'interfaccia in modo da poterla rendere ininfluente ai fini dell'isolamento che stiamo cercando di ottenere.
Supponiamo di avere un servizio che recupera da un database il testo contenuto in una determinata pagina della nostra applicazione:
public class TestiWebServices
{
public TestoDTO RecuperaTesto(string id)
{
// recupera il testo di id specificato dal database
// e lo restituisce
}
}
Il nostro controller, prendiamo ad esempio la pagina "Chi Siamo", che vuole utilizzare questo servizio per recuperare il testo descrittivo dell'azienda farà qualcosa del tipo:
public class ChiSiamoController : Controller
{
public ActionResult Index()
{
TestiWebServices TestiServices = new TestiWebServices();
TestoDTO testo = TestiServices.RecuperaTesto("chisiamo");
return View(testo);
}
}
Se volessimo testare il metodo Index
di questo controller in questo modo saremmo indissolubilmente legati all'implementazione del servizio che, se per qualche ragione fosse non corretta, farebbe fallire il nostro test. Inoltre non ci farebbe rilevare il fatto che in realtà non è il metodo testato ad eseguire un'operazione non corretta, bensì una sua dipendenza.
Applichiamo il DIP estraendo l'interfaccia dalla definizione della classe e demandando al costruttore del controller la creazione dell'istanza:
public interface ITestiWebServices
{
TestoDTO RecuperaTesto(string id);
}
public class TestiWebServices : ITestiWebServices
{
public TestoDTO RecuperaTesto(string id)
{
// recupera il testo di id specificato dal database
// e lo restituisce
}
}
public class ChiSiamoController : Controller
{
private ITestiWebServices testiServices;
public ChiSiamoController()
{
this.testiServices = new TestiWebServices();
}
public ChiSiamoController(ITestiWebServices testiServices)
{
this.testiServices = testiServices;
}
public ActionResult Index()
{
TestoDTO testo = this.testiServices.RecuperaTesto("chisiamo");
return View(testo);
}
}
In questo modo, di default il costruttore senza parametri usa l'implementazione reale del servizio, un'eventuale unit test può "iniettare" la dipendenza utilizzando il secondo costruttore passando magari come parametro un'implementazione fake che non fa altro che restituire una stringa nota.