Lets get one thing out the way, yes I’m a geek, maybe not a super geek but a geek nevertheless. For me Scott Guthrie is up there with my Barca heroes Xavi, Iniesta Messi, Henry, Puyol, Valdés, Busquets, Krkić etc.
Yesterday I went to Scott Gu’s demo of ASP.NET MVC 2 (using the preview 2 release) in London and got to meet Mr ASP.NET MVC
It’s interesting to see that Microsoft is taking rapid web development and templating so seriously. Using the new UI helpers you can create html for labels, inputs and validation with just one line of code in your view instead of three.
This will save you time and mean there is less chance of having any html typos. It will also make it easier to refactor because everything is strongly typed! If you wanted to do this now you could create an html helper extension.
What you can’t do now (not easily anyway) is just have the UI helpers create the label, input and validation for an entire model with one line of code. This will save you even more time and means you don’t have to manually edit html in views just because you have added new properties to your view model.
Amazing I think you will agree, however I can’t help thinking it’s not going to be testable and could potentially lead to inadvertently displaying something you didn’t want to show.
Scott also showed how to use System.ComponentModel.DataAnnotations for validation and the client side validation which uses the jQuery validation framework. If you want that functionality now checkout Steve Sanderson’s excellent xVal framework.
I asked Scott if there was a possibility of not having ‘magic strings’ in action links and the render partial methods. He said they were working on it, so hopefully we could see something in preview 3.
As for release dates, Scott Gu suggested ASP.NET MVC 2 should be ready early next year.
I have no doubt that ASP.NET MVC 2 will make it even better to develop web sites with.
No blog post can cover ASP.NET MVC controller best practices in one go. So this is the first post in a series about ASP.NET MVC controller best practices.
In this post we’re looking at why your ASP.NET MVC controllers should be ‘skinny’.
The code used in this post can be downloaded here.
I’ve heard Jeffrey Palermo say if you can’t see a ASP.NET MVC action method on a screen without having to scroll, you have a problem. In most cases the problem is the controller has multiple responsibilities.
The code in this post is adapted from a code sample I saw in an ASP.NET MVC book.
The controller looks like this
using System;
using System.Linq;
using System.Web.Mvc;
using TestingMvcControllers.Interfaces;
using TestingMvcControllers.Models;
namespace TestingMvcControllers.Controllers
{
public class AnimalsController : Controller
{
public int PageSize = 4;
private readonly IAnimalsRepository _animalsRepository;
public AnimalsController(IAnimalsRepository animalsRepository)
{
_animalsRepository = animalsRepository;
}
public ViewResult List(string category, int page)
{
var animalsInCategory = (category == null)
? _animalsRepository.Animals
: _animalsRepository.Animals.Where(x => x.Category == category);
int numberOfAnimals = animalsInCategory.Count();
ViewData["TotalPages"] = (int)Math.Ceiling((double)numberOfAnimals / PageSize);
ViewData["CurrentPage"] = page;
ViewData["CurrentCategory"] = category;
return View(animalsInCategory
.Skip((page - 1) * PageSize)
.Take(PageSize)
.ToList()
);
}
}
}
This approach will work, but what if you wanted to implement paging for another user interface? You would not be able to reuse the paging functionality in the controller and have to repeat yourself. This goes against the ‘Don’t Repeat Yourself (DRY) principle.
At this point some of you maybe be saying you ain’t gonna need it (YAGNI). Maybe… but let’s have a look at impact paging logic in the controller has when it comes to the unit testing the ASP.NET MVC controller.
using System.Collections.Generic;
using NUnit.Framework;
using Rhino.Mocks;
using TestingMvcControllers.Controllers;
using TestingMvcControllers.Interfaces;
using TestingMvcControllers.Models;
namespace TestingMvcControllers.Tests
{
[TestFixture]
public class AnimalsControllerTests
{
private IAnimalsRepository _animalsRepository;
private AnimalsController _controller;
[SetUp]
public void SetUp()
{
_animalsRepository = MockRepository.GenerateStub<IAnimalsRepository>();
_controller = new AnimalsController(_animalsRepository);
}
[Test]
public void List_Presents_Correct_Page_Of_Animals()
{
// Arrange
_controller.PageSize = 3;
// Act
_animalsRepository.Stub(a => a.Animals).Return(AnimalsStub.Animals);
var result = _controller.List(null, 2);
// Assert
Assert.IsNotNull(result, "Didn't render view");
var Animals = result.ViewData.Model as IList<Animal>;
Assert.AreEqual(2, Animals.Count, "Got wrong number of animals");
Assert.AreEqual(2, (int)result.ViewData["CurrentPage"], "Page number was wrong");
Assert.AreEqual(2, (int)result.ViewData["TotalPages"], "Page count was wrong");
Assert.AreEqual("Iguana", Animals[0].Name);
Assert.AreEqual("Loin", Animals[1].Name);
}
[Test]
public void List_Includes_All_Animals_When_Category_Is_Null()
{
// Arrange
_controller.PageSize = 10;
// Act
_animalsRepository.Stub(a => a.Animals).Return(AnimalsStub.Animals);
var result = _controller.List(null, 1);
// Assert
Assert.IsNotNull(result, "Didn't render view");
var Animals = (IList<Animal>)result.ViewData.Model;
Assert.AreEqual(5, Animals.Count, "Should have got 5 animals");
}
[Test]
public void List_Filters_By_Category_When_Requested()
{
// Arrnage
_controller.PageSize = 10;
// Act
_animalsRepository.Stub(a => a.Animals).Return(AnimalsStub.Animals);
var result = _controller.List("Reptile", 1);
// Assert
Assert.IsNotNull(result, "Didn't render view");
var Animals = (IList<Animal>)result.ViewData.Model;
Assert.AreEqual(3, Animals.Count, "Should have got 3 animals");
Assert.AreEqual("Crocodile", Animals[0].Name);
Assert.AreEqual("Snake", Animals[1].Name);
Assert.AreEqual("Iguana", Animals[2].Name);
Assert.AreEqual("Reptile", result.ViewData["CurrentCategory"]);
}
}
}
Because the controller is also responsible for paging, you have to write unit tests for paging logic on top of the unit tests you should be writing for the controller. This means you will either ending up not testing enough and/or too much and find the tests a nightmare to maintain in the future. See my post about How to Unit Test ASP.NET MVC Controllers.
How I would refactor
I would move the paging functionality into a new layer. Yes I know adding another layer is always the solution to any problem in software development
The animal service layer looks like this
using System;
using System.Linq;
using TestingMvcControllers.Interfaces;
using TestingMvcControllers.Models;
namespace TestingMvcControllers.Services
{
public class AnimalsService : IAnimalsService
{
private readonly IAnimalsRepository _animalsRepository;
public int PageSize = 4;
public AnimalsService(IAnimalsRepository animalsRepository)
{
_animalsRepository = animalsRepository;
}
public AnimalsDto GetAnimals(string category, int page)
{
AnimalsDto animalsDto = new AnimalsDto();
var animalsInCategory = (category == null)
? _animalsRepository.Animals
: _animalsRepository.Animals.Where(x => x.Category == category);
int numberOfAnimals = animalsInCategory.Count();
animalsDto.CurrentCategory = category;
animalsDto.CurrentPage = page;
animalsDto.TotalPages = (int)Math.Ceiling((double)numberOfAnimals / PageSize);
animalsDto.Animals = animalsInCategory
.Skip((page - 1)*PageSize)
.Take(PageSize)
.ToList();
return animalsDto;
}
}
}
Notice how a ‘Dto’ object is returned from the service. While this should be done using an AutoMapper, it’s not the focus of this test. The total number of pages can now be returned to the view as part of the model, rather than as a dictionary entry in ViewData.
The controller now looks clean and simple
using System.Web.Mvc;
using TestingMvcControllers.Interfaces;
namespace TestingMvcControllers.Controllers
{
public class BetterAnimalsController : Controller
{
private readonly IAnimalsService _animalsService;
public BetterAnimalsController(IAnimalsService animalsService)
{
_animalsService = animalsService;
}
public ViewResult List(string category, int page)
{
var animalsDto = _animalsService.GetAnimals(category, page);
return View("List",animalsDto);
}
}
}
The unit tests for the controller are about testing the controller and nothing more!
using MvcContrib.TestHelper;
using NUnit.Framework;
using Rhino.Mocks;
using TestingMvcControllers.Controllers;
using TestingMvcControllers.Interfaces;
using TestingMvcControllers.Models;
namespace TestingMvcControllers.Tests
{
[TestFixture]
public class BetterAnimalsControllerTests
{
private IAnimalsService _animalsService;
private BetterAnimalsController _controller;
[SetUp]
public void SetUp()
{
_animalsService = MockRepository.GenerateStub<IAnimalsService>();
_controller = new BetterAnimalsController(_animalsService);
}
[Test]
public void The_List_Action_Returns_AnimalsDto_To_The_View()
{
// Arrange
_animalsService.Stub(a => a.GetAnimals(null,0))
.IgnoreArguments()
.Return(new AnimalsDto());
// Act
var result = _controller.List(null, 0);
// Assert
result.AssertViewRendered().WithViewData<AnimalsDto>();
}
[Test]
public void The_List_Action_Returns_List_View()
{
// Arrange
_animalsService.Stub(a => a.GetAnimals(null, 0))
.IgnoreArguments()
.Return(new AnimalsDto());
// Act
var result = _controller.List(null, 0);
// Assert
result.AssertViewRendered().ForView("List");
}
}
}
Jag Reehal’s Final Thought on ‘Skinny ASP.NET MVC Controllers’
There is of course more than one way to skin a cat. This solution could have been refactored in many different ways.
The refactoring done in the post means:
The ASP.NET MVC controller is skinnier with less responsibility which means it’s easier to read and maintain
The controller unit tests can focus on the ‘subject under test’
The paging functionality can be used by multiple user interfaces
Given one of the major advantages ASP.NET MVC has over traditional ASP.NET web forms is testability, it’s surprising how many code samples in books and on the web don’t cover how to unit test controllers. There are also many examples of unit tests for ASP.NET MVC controllers that don’t do enough, or do to much.
I want to show examples of good unit tests for ASP.NET MVC controllers and what to avoid so you don’t end up testing more than you should.
The code used in this post can be downloaded here.
Let me start off by discussing what types of unit tests you should be creating for MVC controllers.
Tests to check the correct action result is returned from a controller action. This includes information about the action result, such as the testing the correct view is returned for a view result.
Tests to check if the view model is what you expected. If you have a strongly typed view which expects class foo and you pass class bar to your view model, your code will compile, would result in a runtime error like the one shown below.
If you are testing anything more than this your controller is doing too much. One of the fundamental design practices for MVC controllers is too have skinny controllers. You should you should be able to view a controller method without having to scroll. This will be the focus of my next blog. On that subject you will notice examples in this post don’t include action filters, I think they should be tested separately and is something for a future blog post.
In the unit tests examples below you will see one version using the MvcContrib.TestHelper assembly, and another without it. I can’t recommend this enough because it saves you from writing as many extension methods and test helpers in tests to keep you code clean, readable and maintainable. This is even more important when it comes to doing test driven development because it allows you to concentrate on writing better unit tests without having write more lines of code than you need to.
Also notice how I create a variable for the expected view and route names for the unit tests not using the MvcContrib.TestHelper. Now if I decide to rename the view or route name I only have to change the code in one place.
Unit tests to check an ASP.NET MVC controller returns the correct view
When a controller has no conditional logic i.e. single code path that only returns a single action result, the unit test is straightforward.
Controller Action:
public ActionResult Index()
{
return View("Index");
}
Unit Test:
[Test]
public void Default_Action_Returns_Index_View()
{
// Arrange
const string expectedViewName = "Index";
var customersController = new CustomerController(null);
// Act
var result = customersController.Index() as ViewResult;
// Assert
Assert.IsNotNull(result, "Should have returned a ViewResult");
Assert.AreEqual(expectedViewName, result.ViewName, "View name should have been {0}", expectedViewName);
}
[Test]
public void Default_Action_Returns_Index_View_Using_MvcContrib_TestHelper()
{
// Arrange
var customersController = new CustomerController(null);
// Act
var result = customersController.Index();
// Assert
result.AssertViewRendered().ForView("Index");
}
Unit tests to check an ASP.NET MVC controller sets ViewData (and TempData) values
Testing if a controller sets ViewData or TempData values is simple enough. In the example below the page number and page size are passed to the controller, but need to be passed back to the view. Afterall ASP.NET MVC web sites should to be stateless. In another blog post where controller best practices will be covered, I’ll show you how and why details such as the page number and page size should be properties in the model passed to the view, rather than as ViewData dictionary values and entering magic string territory.
[Test]
public void The_List_Action_Returns_The_Page_Number_And_Size_In_ViewData()
{
// Arrange
const int pageNumber = 1;
const int pageSize = 10;
var customerService = MockRepository.GenerateStub<ICustomerService>();
var customersController = new CustomerController(customerService);
// Act
var result = customersController.List(pageNumber, pageSize);
// Assert
Assert.AreEqual(pageNumber, result.ViewData["pageNumber"], "Page Number Was Incorrect");
Assert.AreEqual(pageSize, result.ViewData["pageSize"], "Page Size Was Incorrect");
}
Unit tests to check an ASP.NET MVC controller returns the correct type of object to a strongly typed view model
It’s good practice to use strong typed views so you need to ensure the MVC controller returns the model the view is expecting. Once written this unit test can be a useful regression test just in case a colleague changes what model a controller passes to a view.
[Test]
public void The_List_Action_Returns_IList_Customers_To_The_View()
{
// Arrange
var customerService = MockRepository.GenerateStub<ICustomerService>();
var customersController = new CustomerController(customerService);
// Set up result for customers service
customerService.Stub(c => c.Get(null, null))
.IgnoreArguments()
.Return(new List<Customer>());
// Act
var result = customersController.List(null, null);
// Assert
var model = result.ViewData.Model as IList<Customer>;
Assert.IsNotNull(model, "Model should have been type of IList<Customer>");
}
[Test]
public void The_List_Action_Returns_IList_Customers_To_The_View_Using_MvcContrib_TestHelper()
{
// Arrange
var customerService = MockRepository.GenerateStub<ICustomerService>();
var customersController = new CustomerController(customerService);
// Set up result for customers service
customerService.Stub(c => c.Get(null, null))
.IgnoreArguments()
.Return(new List<Customer>());
// Act
var result = customersController.List(null, null);
// Assert
result.AssertViewRendered().WithViewData<IList<Customer>>();
}
Unit tests to check an ASP.NET MVC controller returns the expected action result depending on the model state e.g. unit testing the Post-Redirect-Get pattern
When validating a users input you want your controller to return the appropriate action result depending the model state. For this you should follow the Post-Redirect-Get pattern where a valid form submission results in a RedirectToRouteResult, and an invalid form submission should return a ViewResult. The example below shows how to unit test the Post-Redirect-Get pattern for a MVC controller.
Pay attention to how you can add an error to the model state collection. This means you don’t have to worry about what makes a model invalid.
However to ensure you pass in a valid model I would use a test stub which could be used whenever you need an instance of a valid model.
public ActionResult Create(Customer customer)
{
if (ModelState.IsValid)
{
_customerService.Save(customer);
return RedirectToRoute("CustomerCreated");
}
return View("Create", customer);
}
Unit Tests:
[Test]
public void The_Add_Customer_Action_Returns_RedirectToRouteResult_When_The_Customer_Model_Is_Valid()
{
// Arrange
const string expectedRouteName = "CustomerCreated";
var customer = CustomerStub.ValidCustomer;
var customerService = MockRepository.GenerateStub<ICustomerService>();
var customersController = new CustomerController(customerService);
// Act
var result = customersController.Create(customer) as RedirectToRouteResult;
// Assert
Assert.IsNotNull(result, "Should have returned a RedirectToRouteResult");
Assert.AreEqual(expectedRouteName, result.RouteName, "Route name should have been {0}", expectedRouteName);
}
[Test]
public void The_Add_Customer_Action_Returns_RedirectToRouteResult_When_The_Customer_Model_Is_Valid_Using_MvcContrib_TestHelper()
{
// Arrange
var customer = CustomerStub.ValidCustomer;
var customerService = MockRepository.GenerateStub<ICustomerService>();
var customersController = new CustomerController(customerService);
// Act
var result = customersController.Create(customer);
// Assert
result.AssertActionRedirect().RouteName.ShouldBe("CustomerCreated");
}
[Test]
public void The_Add_Customer_Action_Returns_ViewResult_When_The_Customer_Model_Is_Invalid()
{
// Arrange
const string expectedViewName = "Create";
var customer = new Customer();
var customerService = MockRepository.GenerateStub<ICustomerService>();
var customersController = new CustomerController(customerService);
customersController.ModelState.AddModelError("A Error", "Message");
// Act
var result = customersController.Create(customer) as ViewResult;
// Assert
Assert.IsNotNull(result, "Should have returned a ViewResult");
Assert.AreEqual(expectedViewName, result.ViewName, "View name should have been {0}", expectedViewName);
}
[Test]
public void The_Add_Customer_Action_Returns_ViewResult_When_The_Customer_Model_Is_Invalid_Using_MvcContrib_TestHelper()
{
// Arrange
var customer = new Customer();
var customerService = MockRepository.GenerateStub<ICustomerService>();
var customersController = new CustomerController(customerService);
customersController.ModelState.AddModelError("A Error", "Message");
// Act
var result = customersController.Create(customer);
// Assert
result.AssertViewRendered().ViewName.ShouldBe("Create");
}
Behaviour tests for ASP.NET MVC controllers
So far I have only covered state based testing. You can write unit tests for your controller to check if a methods was called. For example when a valid model has been passed to a controller the save method should be called. However when an invalid model is passed to a controller the save method should not be called.
public ActionResult Create(Customer customer)
{
if (ModelState.IsValid)
{
_customerService.Save(customer);
return RedirectToRoute("CustomerCreated");
}
return View("Create", customer);
}
Unit Tests:
[Test]
public void The_CustomerService_Save_Method_Is_Called_When_The_Customer_Model_Is_Valid()
{
// Arrange
var customer = CustomerStub.ValidCustomer;
var customerService = MockRepository.GenerateMock<ICustomerService>();
var customersController = new CustomerController(customerService);
// Act
customersController.Create(customer);
// Assert
customerService.AssertWasCalled(c => c.Save(customer));
}
[Test]
public void The_CustomerService_Save_Method_Is_NOT_Called_When_The_Customer_Model_Is_Invalid()
{
// Arrange
var customer = new Customer();
var customerService = MockRepository.GenerateMock<ICustomerService>();
var customersController = new CustomerController(customerService);
customersController.ModelState.AddModelError("A Error", "Message");
// Act
customersController.Create(customer);
// Assert
customerService.AssertWasNotCalled(c => c.Save(customer));
}
Jag Reehal’s Final Thought on ‘How to Unit Test ASP.NET MVC Controllers’
Hopefully you can use the examples I’ve shown here as templates for your ASP.NET MVC controller unit tests.
You can of course combine the tests above and have multiple assertions such as testing the view name and view model in a single test. Just be aware unit test best practices say you should only have one assertion per test. If you have multiple assertions just make sure you output a message for each assertion. This way you will know which assertion failed, like in the example for testing view data above.
Check out my post about ASP.NET MVC best practices for more examples on good ASP.NET MVC controller unit tests.