New to The Big Boy MVC Series?
Read the series from its humble beginnings.
We’re getting pretty close to having a functioning (albeit ugly) website. If you’re starting to get confused, or angry, or whiny, try to hang in there. My advice is always this: if you don’t understand what’s going on, just copy and paste it into your project. Fiddle with it. Break it. Soon enough, you’ll enjoy the invigorating feeling of deep understanding. And, when that feeling comes, you’ll never want to leave the Microsoft stack ever again. Or, you can always try out Ruby on Rails.
Today, we have a pretty simple task. We’ve created our repository. We’ve also created our controllers. It’s time for the two to meet. Like turtles. In a dark closet.
When using our repository in a controller, we want to follow a few simple (opinionated) guidelines:
- For easy testing, we’ll want to be able to inject a mock repository into our controller whenever we feel like it. Hence, we’ll follow the IOC pattern.
- If a particular ActionResult does not require that we create a DataContext, a DataContext shouldn’t be created. This rule is broken by developers more often than you might expect.
- Our controller is a drama queen. So, our repository should make the life of our controller as easy as possible. If we ever catch ourselves doing too much work in our controller to transform repository data, we probably have an inadequacy in our repository that we need to fix.
- As a sidenote, when we’re updating data (i.e. the CUD in CRUD), we should follow the Post-Redirect-Get Pattern.
Alright. Time to get crackin’!
Step 1: Add some code to the TestInit method in your FoodControllerTest class. Since all of our data is related to Food, we’re definitely going to need access to our repository inside of our FoodController. Hence, let’s redesign our FoodController (via TDD) so that it follows the IOC pattern mentioned in our opinionated guidelines.
using System.Web.Routing;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MvcContrib.TestHelper;
using Big.Fat.Dish.Controllers;
using Big.Fat.Dish.Models;
using Rhino.Mocks;
using System.Web.Mvc;
namespace Big.Fat.Dish.Tests.Controllers
{
[TestClass]
public class FoodControllerTest
{
MockRepository mocks;
IRepository repo;
FoodController foodController;
[TestInitialize]
public void TestInit()
{
mocks = new MockRepository();
repo = mocks.DynamicMock<IRepository>();
foodController = new FoodController(repo); //here's the repo injection
MvcApplication.RegisterRoutes(RouteTable.Routes);
mocks.ReplayAll();
}
//... we're only changing the Food constructor, so...
//... unit tests, etc. omitted for the sake of brevity ...
}
}
With the IOC pattern in place, we’ll be able to inject a mock repository into our controller. We can then verify that our controller is invoking the correct repository method (or methods) without actually having to invoke the method (or methods) on a real repository class. Good times.
Step 2: Try to compile. Feel the fail.. Our controller doesn’t follow the IOC pattern yet. That’s okay. We expected that. To fix the “error”, we need to follow the compiler’s instructions–namely, we need to create a FoodController constructor that takes an IRepository as a parameter:
using System.Web.Mvc;
using Big.Fat.Dish.Models;
namespace Big.Fat.Dish.Controllers
{
public class FoodController : Controller
{
IRepository repo;
public FoodController() : this(null) { } //I'll explain this null parameter later
public FoodController(IRepository repo)
{
this.repo = repo;
}
//...
}
}
Step 3: Setup lazy loading for our repository. With lazy loading, our repository class (and thus our DataContext class) will only be instantiated when the repository is beckoned in a controller method:
using System.Web.Mvc;
using Big.Fat.Dish.Models;
namespace Big.Fat.Dish.Controllers
{
public class FoodController : Controller
{
IRepository lazyRepo;
IRepository repo
{
get
{
if (lazyRepo == null)
{
lazyRepo = new Repository();
}
return lazyRepo;
}
}
public FoodController() : this(null) { } //no need to create a repo yet...
public FoodController(IRepository repo)
{
this.lazyRepo = repo;
}
//...
}
}
Step 4: Create unit tests for the Add Food postback method. When a user submits an Add Food Form on the Add Food page, we want that post request to be intercepted by a method (not yet created) in our FoodController. That method will have a Food class as a parameter, and (here’s the real MVC kicker) that Food class will contain all of the field values from the form submitted by the user. We’ll talk more about this form magic in a later post. I promise.
When testing our Add Food post method, then, we’ll want to verify that
- repo.Add() is called.
- repo.Save() is called.
- neither repo.Add() nor repo.Save() is called if a validation error occurs.
- the user is redirected to the Food Added page if the post was a success.
- the user is redirected back to the Add Food page if the post was a failure.
Let’s go ahead and write all of the tests. Here’s our updated FoodControllerTest class in its entirety:
using System.Web.Routing;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MvcContrib.TestHelper;
using Big.Fat.Dish.Controllers;
using Big.Fat.Dish.Models;
using Rhino.Mocks;
using System.Web.Mvc;
namespace Big.Fat.Dish.Tests.Controllers
{
[TestClass]
public class FoodControllerTest
{
MockRepository mocks;
IRepository repo;
FoodController foodController;
Food food;
[TestInitialize]
public void TestInit()
{
mocks = new MockRepository();
repo = mocks.DynamicMock<IRepository>();
foodController = new FoodController(repo);
food = new Food();
MvcApplication.RegisterRoutes(RouteTable.Routes);
mocks.ReplayAll();
}
[TestCleanup]
public void TestCleanup()
{
RouteTable.Routes.Clear();
}
[TestMethod]
public void Controllers_Food_AddFoodUrl_Maps_To_FoodController()
{
"~/add/food/".Route().ShouldMapTo();
}
[TestMethod]
public void Controllers_Food_AddFoodUrl_Maps_To_Food_Add_Action()
{
"~/add/food/".Route().ShouldMapTo(n => n.Add());
}
[TestMethod]
public void Controllers_Food_FoodAddedUrl_Maps_To_FoodController()
{
"~/food/added/100".Route().ShouldMapTo();
}
[TestMethod]
public void Controllers_Food_FoodAddedUrl_Maps_To_Food_Added_Action()
{
"~/food/added/100".Route().ShouldMapTo(n => n.Added(100));
}
[TestMethod]
public void Controllers_Food_FoodDetailsUrl_Maps_To_FoodController()
{
"~/wa/seattle/ettas/100/salmon-on-cherrywood-plank/".Route().ShouldMapTo();
}
[TestMethod]
public void Controllers_Food_FoodDetailsUrl_Maps_To_Food_Details_Action()
{
"~/wa/seattle/ettas/100/salmon-on-cherrywood-plank/".ShouldMapTo(n => n.Details(100));
}
[TestMethod]
public void Controllers_Food_Post_Calls_Repository_Add()
{
repo.Expect(r => r.Add(food));
foodController.Add(food);
mocks.VerifyAll();
}
[TestMethod]
public void Controllers_Food_Post_Calls_Repository_Save()
{
repo.Expect(r => r.Save());
foodController.Add(food);
mocks.VerifyAll();
}
[TestMethod]
public void Controllers_Food_Post_Does_Not_Call_Repository_On_ModelState_Error()
{
repo.Expect(r => r.Add(food)).Repeat.Never();
repo.Expect(r => r.Save()).Repeat.Never();
foodController.ModelState.AddModelError("Name", "Food Name is required");
foodController.Add(new Food());
mocks.VerifyAll();
}
[TestMethod]
public void Controllers_Food_Post_Redirect()
{
ActionResult result = foodController.Add(food);
Assert.IsInstanceOfType(result, typeof(RedirectToRouteResult));
}
[TestMethod]
public void Controllers_Food_Post_Redirects_To_Food_Added_On_Success()
{
var result = foodController.Add(food) as RedirectToRouteResult;
var routeVals = result.RouteValues;
Assert.AreEqual("Added", routeVals["action"]);
}
[TestMethod]
public void Controllers_Food_Post_Redirects_To_Food_Add_On_Error()
{
foodController.ModelState.AddModelError("Name", "Food Name is required");
var result = foodController.Add(food) as RedirectToRouteResult;
var routeVals = result.RouteValues;
Assert.AreEqual("Add", routeVals["action"]);
}
}
}
Step 5: Try to compile. Feel the fail. Looks like it’s time to create our Add Food post method in our FoodController class. Here’s our updated FoodController class in its entirety:
using System;
using System.Web;
using System.Web.Mvc;
using System.Linq.Expressions;
using Big.Fat.Dish.Models;
namespace Big.Fat.Dish.Controllers
{
public class FoodController : Controller
{
IRepository lazyRepo;
IRepository repo
{
get
{
if (lazyRepo == null)
{
lazyRepo = new Repository();
}
return lazyRepo;
}
}
public FoodController() : this(null) { }
public FoodController(IRepository repo)
{
this.lazyRepo = repo;
}
public ActionResult Index()
{
return View();
}
[HttpGet]
public ActionResult Add()
{
return View();
}
[HttpPost]
public ActionResult Add(Food food)
{
if (!ModelState.IsValid)
{
//TODO: add error message
return RedirectToAction("Add");
}
this.repo.Add(food);
this.repo.Save();
//note: I changed FoodId to Id (in db table and dbml file)
return RedirectToAction("Added", new { id = food.Id });
}
public ActionResult Added(int id)
{
return View();
}
public ActionResult Details(int id)
{
return View();
}
}
}
Step 6: Run your unit tests. Feel the breeze in your underpants. We now have a post method in our FoodController that passes all of our unit tests.
That’s it for now.
Tomorrow, we’ll start by cleaning up the actual Add Food page. Then, we’ll get to watch our first successful postback. And, finally, after a wee bit of fireworks and a quick delusional spike in self-esteem, we’ll get right back to work. We’ve only got a couple weeks left to ship this monkey!
Move on to Part 22: Mistakes Make Me Human. Fixing Mistakes Makes Me Brad Wilson.
Read More
You can leave a response, or trackback from your own site.
6 Responses to “The Big Boy MVC Series — Part 21: Controller, Meet Repo. Repo, Meet Controller.”
Leave a Reply




[...] Move on to Step 21: Controller, Meet Repo. Repo, Meet Controller. [...]
hi! great series =)
are we going to add some inversion of control container like ninject later on? if so, how do we resolve the dependencies to these lazy loaded properties inside the getters?
preferrably, there shouldn’t be any references to any IoC container in more than one part of the project, so we should try to do this without for example container specific attributes (i’m currently working on a solution for Ninject, but i’m not getting very far…)
any ideas?
Hey Thomas,
This is a really great question, and something I honestly didn’t think too much about before diving in.
After looking around for some solid Ninject/MVC2 documentation, I’m feeling a bit scattered. Have any recommended links/posts? I’d like to explain how to add this to the project (while maintaining lazy loading), but I need to wrap my head around some more Ninject best practices first.
Thanks again for the suggestion.
Cheers,
Evan
hi evan,
i’m glad you like my suggestion – that bumps up my chances of finding a way to do it =)
the best guide to Ninject that i’ve found so far has been the official wiki on ninject’s github site (go to ninject.org, click “dojo” then “official wiki” and you’re there) but most of the stuff there is pretty basic. there’s some pages toward the end about internal stuff going on when resolving dependencies, but it seems that the documentation most of the time assumes that you’re using the [Inject] attribute – which we don’t want to.
there is something about injecting without the attributes when using constructor injection at the top of (1), but i have found nothing like it for property injection (which really is what we want to do).
(1) http://wiki.github.com/ninject/ninject/injection-patterns
when i first started with DI, i chose ninject because it seemed lightweight and had an easy-to-understand api, but it could well be that the framework itself is too limited for this.
there’s a google news group (or whatever they’re called) and a couple of blogs too, but I haven’t looked into those very much. all the links can be found on ninject.org.
// tomas
update: there seems to be quite a lot of questions about this very topic, and some solutions suggested too, in the google group. see http://groups.google.com/group/ninject/search?group=ninject&q=property+injection+without+inject+attribute&qt_g=Search+this+group for the search results
best regards,
tomas
[...] notice that Kazi recommends that you implement the PRG pattern (something I blogged about here). In laymen’s terms: when the user posts data, we’ll verify the data. If the data sucks [...]