I can’t say I like all the aspects of Microsoft ASP.Net MVC. But there is one aspect that I like though is the ability to unit test most of the components of your application. Standard ASP.Net did not prevent you from testing your application. However, the framework and documentation did not encourage you to organize your application in a way that is testable.
ASP.Net MVC really pushes for loosely coupled components and as such, encourages unit testing. Your controllers are not much more than plain methods taking some input, executing some logic and returning an output. Your controller is not responsible for much in the end. Even though it is a central part of any functionality, its responsibility is reduced to implementing the logic for responding to a certain type of request. It relies on the framework configuration and conventions to call its methods in an appropriate way.
Understanding the extend of a controller’s responsibility is a key to writing good, concise unit tests. Sometimes, it’s easier to remember what it is not responsible for:
- Model binding: turning the encoded form data or routing information into .Net object. A controller action does not need to know and does not care that the id of the object your are updating is a part of your action’s path (eg.
/admin/tags/edit/my-tag ) or if it is provided in the form of an encoded form field (eg.
<input type="text" name="tagSlug" /> )
- Model validation: it may seem surprising but in most cases, your controller should not implement the validation logic. From a controller’s action point of view, is there really a difference between a person’s last name missing or an unknown phone number format ? As far as your hypothetical EditPerson action is concerned, there is a validation error. Model validation should be tested separately.
- Pure business logic: a controller action is meant to handle requests. Even though it does not directly cope with http intricacies, it still very close to the http request/response life cycle. For example, an action ComputeLoanSchedule that needs to return a loan amortization schedule should probably delegate the actual computation to a business service class (ILoanService.GetAmortizationSchedule()) whose sole purpose is to handle such computation. In the future, if you need to expose the amortization schedule feature as a web api, you will only need to implement another controller and call the same business service.
In the end, your controller is only responsible for:
- Delegating work to domain services: as stated above, the domain logic of your application should be logically separated from your UI layer. It also gives you the flexibility to scale your web application and business domain services.
- Returning an appropriate response: a controller’s action responds to a request by returning a response. The standard MVC Controller class expects your actions provide a result that will drive the way the response if created. eg. Returning a ViewResult will trigger view rendering and a RedirectToRouteResult may respond with an HTTP 302.
Unit testing a controller action
When it comes to unit testing, the less to test, the better. As such, the limited responsibility of controller actions is a boon when writing unit tests. As an example, I will be using a very simple controller
TagsController . It is part of a blog-like website. Its role is to allow for the management of tags to be applied to other components of the application like articles etc.
TagsController exposes a CRUD-like set of functionality:
- Index: provides the user with a list of existing tags
- Create: enables the creation of new tags
- Edit: enables editing existing tags
The simple case
We will focus on the index functionality for now. It is implemented as a single action on our TagsController:
|
public class TagsController : Controller { private ITagService tagService; public TagsController(ITagService tagService) { this.tagService = tagService; } public ActionResult Index() { var tags = this.tagService.GetAll().OrderBy(tag => tag.Name); return View("Index", tags); } /* ... */ } |
The TagsController constructor takes a ITagService. It provides our constructor access to the tags in our database. As you can see, the
Index() action method does only 2 distinct operations. First, it asks for the tags to display. Then, it tells the framework to render the “Index” view using the retrieved tags as a model.
With such a simple implementation, the unit test will be quite simple also. I’ll follow the usual AAA (Arrange-Act-Assert) pattern.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
[Test] public void Index_RetrievesAllTags() { // Arrange var mockService = new Mock<ITagService>(); var controller = new TagsController(mockService.Object); mockService .Setup(s => s.GetAll()) .Returns(new[] { new Tag {Slug = "tag1", Name = "Tag1"}, new Tag {Slug = "tag7", Name = "Tag7"}, new Tag {Slug = "tag4", Name = "Tag4"} }); // Act var result = controller.Index() as ViewResult; // Assert Expect(result, Not.Null); Expect(result.ViewName, EqualTo("Index")); // ensure service was used mockService.Verify(s => s.GetAll(), Times.Once()); // check view's model var tags = result.Model as IEnumerable<Tag>; Expect(tags, Not.Null); Expect(tags.Count(), EqualTo(3)); // tags should be sorted Expect(tags.ToArray()[0].Slug, EqualTo("tag1")); Expect(tags.ToArray()[1].Slug, EqualTo("tag4")); Expect(tags.ToArray()[2].Slug, EqualTo("tag7")); } |
The first part sets up an ITagService mock for TagsController to consume. We then simply call method
Index() .
The assertions start with making sure we got a non-null result of the ViewResult type.
Index() should ask for the “Index” view to be rendered. I prefer explicitly specifying the view name to render in my controller action. I believe this reduces the mental gymnastic necessary when debugging action-view interactions.
mockService.Verify() ensures
ITagService.GetAll() was called.
I then proceed with checking the model provided to the view is consistent with the data from the mock service object. One of the requirements is that the tags are sorted by name.
As you can see in the Index unit test, there is a lot more code than the method being tested. This is also one of the reason why you should reduce the scope of your tests as much as you can.
A more complex test case
I’ll cover the “create a new tag” functionality. In this case, the functionality is implemented by a pair of actions. A parameter-less
Create() action simply triggers the rendering of an empty tag editor (implemented by a view names “Save”. The other
Create() action takes a
SaveTagModel parameter and responds to form submissions. As such, it behaves differently in case there is a validation error or the tag already exists in the tag db.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
public class TagsController : Controller { /* ... */ [HttpGet] public ActionResult Create() { return View("Save", new SaveTagModel() {IsNew = true}); } [HttpPost] public ActionResult Create(SaveTagModel saveTagModel) { var existingTag = tagService.GetBySlug(saveTagModel.Slug); if (existingTag != null) { ModelState.AddModelError("TagExists", "A tag with that slug already exists"); } if (!ModelState.IsValid) { saveTagModel.IsNew = true; return View("Save", saveTagModel); } tagService.Save(new Tag() { Name = saveTagModel.Name, Slug = saveTagModel.Slug }); return RedirectToAction("Index"); } /* ... */ } |
I’ll show the tests I put together to cover the functionality of
TagsController.Create() . The first of those test is ensuring the initial to Create() triggers the rendering of an empty tag editor. The tag editor is implemented by a view called “Save” shared between the
Create() and
Edit() operations. As you can see, I make sure the view is specified by name. I also make sure the model is in a state consistent with an empty
SaveTagModel .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
[Test] public void Create_ShowsEmptySaveEditor() { // Arrange var mockService = new Mock<ITagService>(); var controller = new TagsController(mockService.Object); // Act var result = controller.Create() as ViewResult; //Assert Expect(result, Not.Null); Expect(result.ViewName, EqualTo("Save")); var saveTagModel = result.Model as SaveTagModel; Expect(saveTagModel, Not.Null); Expect(saveTagModel.IsNew, True); Expect(saveTagModel.Name, Null.Or.Empty); Expect(saveTagModel.Slug, Null.Or.Empty); } |
The next 2 tests cover the behavior of the
Create(SaveTagModel tag) action. That is the action that responds to form submission from the tag editor. These tests need to cover the following
- What happens when invalid input is provided?
- What happens when a tag exists with the same ‘slug’?
- What happens upon success?
It will not come as a surprise that I wrote 3 tests, one for each of the points. The first test ensure
Create() behaves when provided with invalid data. This test demonstrates an important point. The controller is not aware and does not care what the actual model errors are. Its ‘invalid input’ behavior is triggered by any model error. We want to reduce our controller logic as much as possible. There may be some cases where the controller’s action behave differently based on specific errors. If you can avoid it, do so. It is against the separation of concern. A controller is not responsible for validation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
[Test] public void Create_ShowsEditorAgainOnInvalidInput() { // Arrange var mockService = new Mock<ITagService>(); var controller = new TagsController(mockService.Object); // Act var saveTagModelArg = new SaveTagModel() {Slug = string.Empty, Name = "somename"}; controller.ModelState.AddModelError("testerror", "test message"); var result = controller.Create(saveTagModelArg) as ViewResult; //Assert Expect(result, Not.Null); Expect(result.ViewName, EqualTo("Save")); var saveTagModel = result.Model as SaveTagModel; Expect(saveTagModel, Not.Null); Expect(saveTagModel.IsNew, True); Expect(saveTagModel.Name, EqualTo("somename")); Expect(saveTagModel.Slug, Null.Or.Empty); Expect(result.ViewData.ModelState.IsValid, False); } |
Considering what I have just said, there is an issue with the current implementation of
Create() . It checks that a tag does not exist. This is a form of validation and should probably be moved either to model validation (implemented by a custom validation attribute) or to the service (by adding a specific
ITagService.Create() method). On the other hand, since the validation relies on a service component one could argue that it is part of the orchestration the controller is responsible for. I will leave it here because it is such a trivial validation. Anything more complex I would extract it and test it separately. My rule of thumb is: if it takes more than one unit test to cover a piece of validation in the controller, the validation should be moved to its own class.
Here is the test that covers that part.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
[Test] public void Create_ShowsEditorAgainOnDuplicateSlug() { // Arrange var mockService = new Mock<ITagService>(); var controller = new TagsController(mockService.Object); mockService.Setup(s => s.GetBySlug("tag")) .Returns(new Tag("tag", "Tag")); // Act var saveTagModelArg = new SaveTagModel() {Slug = "tag", Name = "somename"}; var result = controller.Create(saveTagModelArg) as ViewResult; //Assert Expect(result, Not.Null); Expect(result.ViewName, EqualTo("Save")); var saveTagModel = result.Model as SaveTagModel; Expect(saveTagModel, Not.Null); Expect(saveTagModel.IsNew, True); Expect(saveTagModel.Name, EqualTo("somename")); Expect(saveTagModel.Slug, EqualTo("tag")); Expect(result.ViewData.ModelState.IsValid, False); } |
Last but not least, here is the test that covers the success path. Out action should have called
ITagService.Save() with an appropriate argument and it should redirect us to the index page for our tags. As you can see, the redirection is tested against route values, not against an actual URI. The routing configuration is covered by another set of tests. This will be the subject of another post in the next few days.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
[Test] public void Create_SavesTagAndRedirectsOnSuccess() { // Arrange var mockService = new Mock<ITagService>(); var controller = new TagsController(mockService.Object); var saveTagModelArg = new SaveTagModel() {Slug = "tag", Name = "somename"}; mockService.Setup(s => s.Save(It.Is<Tag>(tag => tag.Slug == "tag" && tag.Name == "somename"))); // Act var result = controller.Create(saveTagModelArg) as RedirectToRouteResult; //Assert Expect(result, Not.Null); Expect(result.Permanent, False); Expect(result.RouteName, Null.Or.Empty); Expect(result.RouteValues["controller"], Null.Or.Empty.Or.EqualTo("Tags")); Expect(result.RouteValues["action"], EqualTo("Index")); mockService.VerifyAll(); } |
Conclusion
As you can see, even though our controller is quite simple (3 methods only and simple at that), we had to write quite a few lines of unit test code to cover all the code paths. If you want to keep your unit tests to a minimum, make sure you respect the separation of concern principle. Test each of the involved components separately and reduce to a minimum the contract between them. The less they know about the other, the easier it is to change one component without your change to ripple through your whole application.
In the next few posts, I will cover testing model binding and validation as well as routes.
Don’t hesitate to hail me in the comments or on Twitter