How to Unit Test ASP.NET MVC Controllers

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.

ASP NET MVC Runtime Error

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.

If you’re new to mocking frameworks and stubs, check out my beginners guide to mocking frameworks.

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.

Controller Action:

public ViewResult List(int? pageNumber, int? pageSize)
{
    IList<Customer> customers = _customerService.Get(pageNumber, pageSize);

    ViewData["PageNumber"] = pageNumber;
    ViewData["PageSize"] = pageSize;

    return View("List", customers);
}

Unit Test:

[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.

Controller Action:

public ViewResult List(int? pageNumber, int? pageSize)
{
    IList<Customer> customers = _customerService.Get(pageNumber, pageSize);

    ViewData["PageNumber"] = pageNumber;
    ViewData["PageSize"] = pageSize;

    return View("List", customers);
}

Unit Test:

[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.

Comments

  • Pingback: Reflective Perspective - Chris Alcock » The Morning Brew #435

  • Patrice Calvé

    Hi,

    Great post, thank you.

    At the end of your article, you state that “I will update this post when I see more examples on good ASP.NET MVC controller unit tests worth sharing”, may I suggest that you also include “I will update this post when I see more examples on >>bad<< ASP.NET MVC controllers or unit tests and change them into good ones"

    Pat

  • http://weblogs.asp.net/psteele Patrick Steele

    Once you get into more advanced testing (involving Session, Response, Request, HttpContext, etc…) you’ll find the MvcContrib library on CodePlex very helpful. I blogged about using it here:

    http://weblogs.asp.net/psteele/archive/2009/08/23/asp-net-mvc-mvc-contrib-unit-testing.aspx

  • Russell Cox

    Thanks for the excellent examples

  • http://www.arrangeactassert.com Jag Reehal

    Thanks Patrice. I’m working on a post to do exactly that, should be posted in the next couple of days.

  • Rav Panesar

    I will be using the examples in my code.

    Will you be covering routes and views?

  • http://www.arrangeactassert.com Jag Reehal

    Both are big topics, so it depends on time.

    Until then Ben Scheirman is your man for unit testing ASP.NET MVC Routes. Check out MVC Contrib for updates.

    For views check out Steve Sanderson’s post on First steps with Lightweight Test Automation Framework.

  • http://www.norbi-net.de Norbert Baum

    Hello,
    i have a problem :(

    my Model:
    public class HomeKontakt
    {
    [RegularExpression(@"^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$”, ErrorMessageResourceType = typeof(ModelValidation), ErrorMessageResourceName = “MailInvalid”)]
    [Required(ErrorMessageResourceType = typeof(ModelValidation), ErrorMessageResourceName = "MailRequired")]
    public String Mail { get; set; }
    }

    if my Mail-Adress: baum@bla@evaluate-lesson.de
    so its no valid! Over the Website, the Validation is Correkt and say, that the Mail-Adress not Valid is!

    But in my Tests, they ignor my ValidationRules! Why?
    The Modelstate is clear/ with no Errors

    Can you Help me?

    Thx and greets from Germany
    Norbert

  • http://www.arrangeactassert.com Jag Reehal

    Hi Norbert,

    One thing I would suggest is creating an email attribute instead of the regular expression you’re currently using. See Scott Gu’s post on Model Validation for an example.

    Cheers,

    Jag

  • adam

    very useful thanks

  • ccnweb ccnweb

    I have same problem as Norbert Baum, I try create an custom attribute instead of the regular expression called TiempoEnDiasHorasMinutos, but in test, model state contains nothing, no values, no keys.

  • http://thoughtresults.com/ Saeed Neamati

    Good starting point; but I need more references so that I can draw the big picture of unit testing in my mind. Can you introduce some resources on that?

  • James Fleming

    Thanks for this. I’m crazy busy and needed to get my arms around mocking controllers fast. I truly appreciate your contribution to the community