Development: Hoe versus Wat en Dependency Injection en Unit tests
Het is niet nodig om een te geven per methode welke implementatie we willen gebruiken; in veel gevallen, zoals deze, staat bij het starten van de applicatie al vast welke implementatie van een bepaalde service of interface wordt gebruikt. We kunnen die keuze bepalen in de controller zelf:
public class TemperatureController : Controller
{
ITemperatureDataSource temperatureDataSource = new WebserviceTemperatureDataSource();
public ActionResult GetAverage(string station)
{
double average = temperatureDataSource.GetAverageTemperature(station);
return Json(new{average = average }, JsonRequestBehavior.AllowGet);
}
De interface ITemperatureDatasource moest hiervoor wel worden aangepast: we geven de code van het weerstation niet mee aan de constructor van de datasource, maar aan de methode GetAverageTemperature.
We kunnen ook in de constructor van de controller bepalen welke implementatie we willen gebruiken:
public class TemperatureController : Controller
{
ITemperatureDataSource temperatureDataSource;
public TemperatureController()
{
temperatureDataSource = new WebserviceTemperatureDataSource();
}
Als we nu echter ComponentTemperatureDataSource willen gebruiken, dan moeten we alsnog de controller aanpassen. Niet alleen deze controller, maar mogelijk ook andere. Het zou mooi zijn als we op een hoger niveau kunnen aangeven welke implementatie gebruikt moet worden. Hier geldt ook dat een best practice is om alleen relevante bestanden en componenten aan te passen. Als we slechts een andere implementatie willen toepassen, dan is het niet logisch dat we het bestand of de klasse gaan aanpassen dat alleen maar als doel heeft temperatuurdata terug te geven, ongeacht databron.
Hiervoor kunnen we dependency injection toepassen. Er zijn verschillende oplossingen, zoals Unity (deprecated) of Autofac. Hieronder pas ik Autofac toe:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
/*...*/
var builder = new ContainerBuilder();
builder.RegisterControllers(typeof(MvcApplication).Assembly);
builder.RegisterType<WebserviceTemperatureDataSource>().As<ITemperatureDataSource>();
var container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
}
}
//TemperatureController.cs:
public class TemperatureController : Controller
{
ITemperatureDataSource _temperatureDataSource;
public TemperatureController(ITemperatureDataSource temperatureDataSource)
{
_temperatureDataSource = temperatureDataSource;
}
Unit tests
Het voordeel hiervan is dat we deze methode in deze controller te testen, zónder het component te testen dat wordt gebruikt om de gemiddelde temperatuur op te halen. We willen geen verbinding maken met de webservice, we willen input en output van de controllermethode testen, ervan uitgaande dat de gemiddelde temperatuur X is:
public class TemperatureControllerTest
{
[TestMethod]
public void GetAverageReturnsCorrectJson()
{
Models.ITemperatureDataSource temperatureDataSource = new TestTemperatureDataSource();
string station = "DHL";
TemperatureController controller = new TemperatureController(temperatureDataSource);
JsonResult result = controller.GetAverage(station) as JsonResult;
IDictionary<string, object> wrapper = (IDictionary<string, object>)new System.Web.Routing.RouteValueDictionary(result.Data);
Assert.AreEqual(1.5, wrapper["average"]);
}
}
internal class TestTemperatureDataSource : ITemperatureDataSource
{
public double GetAverageTemperature(string station)
{
return 1.5;
}
}
Nu kunnen we de test aanpassen en de controllers, zonder andere klassen te testen of gebruik te maken van externe systemen zoals databases of webservices. Als we bijvoorbeeld een viewmodel willen gebruiken in de controller, dan kunnen we dit via de unit test in de gaten houden:
// Controller:
public ActionResult GetAverage(string station)
{
double average = _temperatureDataSource.GetAverageTemperature(station);
AverageResult result = new AverageResult(){ average = average };
return Json(result, JsonRequestBehavior.AllowGet);
}
// Test:
JsonResult result = controller.GetAverage(station) as JsonResult;
var r = result.Data as AverageResult;
var json = JsonConvert.SerializeObject(r);
var wrapper = JsonConvert.DeserializeObject<dynamic>(json);
Assert.AreEqual(1.5, (double)wrapper["average"]);
}