How to Unit Test ASP.NET MVC Controllers
TweetGiven 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.
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.
Pingback: Reflective Perspective - Chris Alcock » The Morning Brew #435