Story Details for articles

Golf Tracker - Ep. 9 - Controller Tests

kahanu
Author:
Version:
Views:
3153
Date Posted:
8/24/2011 7:51:18 PM
Date Updated:
8/24/2011 7:51:18 PM
Rating:
0/0 votes
Framework:
ASP.NET MVC 2
Platform:
Windows
Programming Language:
C#
Technologies:
MvcContrib
Tags:
golf, golf tracker, unit tests, controller tests, route tests
Demo site:
Home Page:
Share:

Golf Tracker - Controller Tests

One of the first questions you might ask is why have controller tests?  I just created repository tests, why also create controller tests?

To be clear, there are no requirements that you create any unit tests for your MVC application at all, but since the MVC framework is so good and it was built with unit testing in mind, by creating tests you will only be making your application stronger and more viable. 

But also be aware that unit testing takes time and a certain amount of skill and knowledge.  Depending on the type of testing you want to perform, you will need to know how to operate certain testing frameworks such as NUnit, XUnit, etc.  Not to mention the various mocking frameworks, such as Moq, Rhino Mocks, etc.  Plus you should have a better than basic understanding of HTTP and how it works.

For some testing you'll also need to know some inner workings of the .Net platform for testing IIdentity, or IPrincipal objects, etc.

Once you have all that knowledge stored away in your cranium, you can get started with your testing.

How to test a controller

To test a controller, you should make a list of the things you want to test.  You can test the basic CRUD operations, or you can test that specific types are being passed up to the view, or you can test that exact values in viewmodels are what are expected.

What you test is up to you, but you should be as thorough as you can be.

You should never test against a working data store!

For my controller tests I will start out with some simple tests to make sure some expectations are met.  These will be my first three tests:
  1. Assert that the type passed to the view is of type CourseViewData
  2. Test model validation on a Create operation
  3. Assert that expected data is being returned from the controller
One of the first things I need to do in order to get this to work is to simply add a reference to the MVC application in the UnitTests project.

adding reference to MVC app to unit tests

You can see that I've added the GolfTracker.Mvc.Web project.  This allows me to get access to the controllers and later, the routes.

Next I'll create a class in my Unit Tests folder called CourseControllerTests.cs.

coursecontrollertests class

Inside this class I'll set it up to use NUnit as the testing framework and setup the fake repository for easy data access.  This is what the class looks like after I've set it up.

01.using System;
02.using System.Web.Mvc;
03.using GolfTracker.BusinessObjects;
04.using GolfTracker.DataObjects.Interfaces;
05.using GolfTracker.Mvc.Web.Controllers;
06.using GolfTracker.Mvc.Web.ViewData;
07.using GolfTracker.Services;
08.using GolfTracker.Services.Interfaces;
09.using GolfTracker.UnitTests.Course_Tests;
10.using NUnit.Framework;
11.using NUnit.Framework.SyntaxHelpers;
12. 
13.namespace GolfTracker.UnitTests.ControllerTests
14.{
15.    [TestFixture]
16.    public class CourseControllerTests
17.    {
18.        ICourseRepository GetRepository()
19.        {
20.            return new FakeCourseRepository(new FakeCourseData().GetCourses());
21.        }
22. 
23.        ICourseService GetService()
24.        {
25.            var service = new CourseService();
26.            service.CourseRepository = GetRepository();
27.            return service;
28.        }
29. 
30.        CourseController GetController()
31.        {
32.            var controller = new CourseController();
33.            controller.service = GetService();
34.            return controller;
35.        }
36. 
37. 
38.    }
39.}

In my unit tests I will be calling the GetController() method which will initiate pseudo-dependency injection for the services and the repositories.  This helps keep my tests clean and easy to manage.

Now I can add my first test.  I want to test that when I call the Index action method that a CourseViewData class is being passed to the view.  I want to make sure that all my unit tests methods names are assertive, meaning they all are descriptive in what they are testing.  This way I can look at them in NUnit GUI, and see all the tests by name and know what they are testing.

01.[Test]
02.public void Index_Method_Returns_CourseViewData()
03.{
04.    // Arrange
05.    var controller = GetController();
06. 
07.    // Act
08.    ViewResult result = controller.Index() as ViewResult;
09. 
10.    // Assert
11.    Assert.That(result.ViewData.Model.GetType(), Is.EqualTo(typeof(CourseViewData)));
12.}

In this test I call the Index action on line 8 and return it as a ViewResult.  Then I can drill into that class to get the type that is returned to the view, which is what I'm doing on line 11.  Then I just compare it to what I expect and see what the result is.

This test passes without a problem, but if I change line 11 to be this:

Assert.That(result.ViewData.Model.GetType(), Is.EqualTo(typeof(PlayerViewData)));

... then running the test again will result in this message.

01.------ Test started: Assembly: GolfTracker.UnitTests.dll ------
02. 
03.Test 'M:GolfTracker.UnitTests.ControllerTests.CourseControllerTests.Index_Method_Returns_CourseViewData' failed:   Expected: <GolfTracker.Mvc.Web.ViewData.PlayerViewData>
04.  But was:  <GolfTracker.Mvc.Web.ViewData.CourseViewData>
05. 
06.    NUnit.Framework.AssertionException:   Expected: <GolfTracker.Mvc.Web.ViewData.PlayerViewData>
07.      But was:  <GolfTracker.Mvc.Web.ViewData.CourseViewData>
08.     
09.    at NUnit.Framework.Assert.That(Object actual, Constraint constraint, String message, Object[] args)
10.    at NUnit.Framework.Assert.That(Object actual, Constraint constraint)
11.    ControllerTests\CourseControllerTests.cs(47,0): at GolfTracker.UnitTests.ControllerTests.CourseControllerTests.Index_Method_Returns_CourseViewData()
12. 
13.0 passed, 1 failed, 0 skipped, took 0.25 seconds (Ad hoc).

You can see that the returned type expected is PlayerViewData, but CourseViewData was actually returned.  By calling the GetType() method of the model returned from the ViewResult class, I can easily assert model types and catch any problems that may arise.

My second test will see if the model binding validation is working with the DataAnnotations attributes.  What I'll be testing is whether the validation is caught and the error messages are passed back up to the view if a model doesn't pass validation.

In order to do this properly, we cannot actually expect the DataAnnotations framework to work from a unit test.  The reason why can be more succinctly put reading Brad Wilson's blog post

"The DataAnnotations attributes behave in an AOP-style fashion where their behavior becomes visible only when the whole system is functioning."

But to briefly explain why I need to do some additional massaging for this test, it's explained as, the validation can't work without the complete system available.  In other words, since a unit test is completely outside the MVC framework, the DataAnnotations validation will not work automatically.

So to get this to work, I have to manually include an AddModelError call which simulates the model binder hitting the DataAnnotations class.  Then the controller can take appropriate action.  Here's the new test.

01.[Test]
02.public void Create_Post_Action_Fails_With_Empty_Values()
03.{
04.    // Arrange
05.    var controller = GetController();
06.    var model = new Course();
07. 
08.    // Pseudo-validation
09.    // By the CourseName being empty (null) the ModelState.IsValid would return false
10.    // in the controller, but not in this test environment.
11.    if (model.CourseName == null)
12.    {
13.        controller.ModelState.AddModelError("CourseName", "Course Name is required");
14.    }
15.     
16.    // Act
17.    RedirectToRouteResult result = controller.Create(model) as RedirectToRouteResult;
18. 
19.    // Assert
20.    Assert.That(result.RouteValues["action"], Is.EqualTo("Create"));
21.}

Line 6 creates a new Course model and lines 11 through 14 adds the model error.  Then line 17 can call the Create action and pass in the model and it will return a RedirectToRouteResult

Line 20 asserts the expection that since the model's properties were all empty or null, this is a violation and we should be returned to the Create GET action index of the Index action.

The last of the initial three tests is a test to assert that I'm getting back data that I expect when I call the Edit action.  I'll be simulating calling the Edit GET action method it should return a Course model fully populated with data from the fake repository.  This is possible since I am not using mocking but accessing a fake repository with fake data.

01.[Test]
02.public void Edit_Get_Should_Return_Course4()
03.{
04.    // Arrange
05.    var controller = GetController();
06. 
07.    // Act
08.    ViewResult result =
09.        controller.Edit(new Guid("3591b0e3-a220-409b-9049-efd1eaf53e50")) as ViewResult;
10.    var model = (CourseViewData)result.ViewData.Model;
11. 
12.    // Assert
13.    Assert.That(model.Course.CourseName, Is.EqualTo("Course 4"));
14.}

This kind of test for me is important.  It not only validates that the system is working, but it also validates that expectations are met.

Testing Routes

With the right tools, testing routes is ridiculously easy.  I'm talking about the MvcContrib open source framework.  They have a test helper set of classes that help in various ways.  One of the ways is route testing.

In my mind, route testing is almost more important than controller testing.  If your routes don't work, neither does your application.

Routes are functions that route URLs to controller actions.

So what is necessary to test routes?  Make sure the reference to the MVC application is added to the project. Then create a new unit test class for the route tests.

route tests folder

Now there's a little bit of setup required in order to make this work as necessary.  What I can do is create a set of routes to test against and put them in my unit tests folder.  But what I really want to do is test against my live routes in the Global.asax file of my MVC application.  This way, like the controllers, I can test the actual implementation of my MVC application.

Here's my implementation of my route testing class with a few simple route tests.

01.using System;
02.using System.Web.Routing;
03.using GolfTracker.Mvc.Web;
04.using GolfTracker.Mvc.Web.Controllers;
05.using MvcContrib.TestHelper;
06.using NUnit.Framework;
07. 
08.namespace GolfTracker.UnitTests.RouteTests
09.{
10.    [TestFixture]
11.    public class CourseRouteTests
12.    {
13. 
14.        [TestFixtureSetUp]
15.        public void FixtureSetup()
16.        {
17.            RouteTable.Routes.Clear();
18.            MvcApplication.RegisterRoutes(RouteTable.Routes);
19.        }
20. 
21.        [Test]
22.        public void Course_Should_Map_To_Index()
23.        {
24.            "~/Course".ShouldMapTo<CourseController>(a => a.Index());
25.        }
26. 
27.        [Test]
28.        public void Edit_Route_Should_Map_To_Edit_Method()
29.        {
30.            "~/Course/Edit/3591b0e3-a220-409b-9049-efd1eaf53e50"
31.                .ShouldMapTo<CourseController>(a =>
32.                    a.Edit(new Guid("3591b0e3-a220-409b-9049-efd1eaf53e50")));
33.        }
34. 
35.        [Test]
36.        public void Create_Route_Should_Map_To_Create_Method()
37.        {
38.            "~/Course/Create".ShouldMapTo<CourseController>(a => a.Create());
39.        }
40.    }
41.}

Lines 14 through 19 setup the test class to gain access to the RouteTable from the Global.asax class. Then I create tests using the MvcContrib.TestHelper classes.  The helper classes have a static method called ShouldMapTo<controller name>() where you can call a Lambda expression for the action method.  This gets tacked right onto the string representation of the route as it would appear in a link or URL.

The only thing that this does not have, is the way to follow the "Triple A" pattern, Arrange, Act, Assert.  This isn't such a bad thing, because this will really tell you if something is not working correctly in your route.

Conclusion

Unit test is not simple nor is it fun, but it doesn't have to be that difficult and the more you educate yourself on how to do unit testing, the easier it becomes, and the more stable your applications become.

Stay tuned.

Comments

    No comments yet.

 

User Name:
(Required)
Email:
(Required)