Using Extension Methods to Clean Up Tests
Wednesday, September 10 2008 2 Comments
A lot of unit tests for ASP.NET MVC projects will look a lot like this:
[Test]public void list_action_should_render_default_view(){ var controller = CreateProductController(); //defined elsewhere var result = controller.List(); Assert.That(result, Is.TypeOf(typeof(ViewResult)); var viewResult = (ViewResult)result; Assert.That(viewResult.ViewName, Is.EqualTo(string.Empty));}We basically create the controller, invoke the desired action, and verify that the result is of type ViewResult, and that the name of the view is an empty string.
This test isn't necessarily all bad. It is easy to read and consists of only 5 lines of code. Here's another test, this time testing that an action redirects the user:
[Test]public void login_action_should_redirect_to_home_index_on_successful_login(){ string username = "bob"; string password = "pass123"; var loginService = MockRepository.GenerateStub(); loginService.Stub(x => x.Authenticate(username, password).Return(true); var controller = new AccountController(loginService); var result = controller.Login(username, password); Assert.That(result, Is.TypeOf(typeof(RedirectToRouteResult))); var redirectResult = (RedirectToRouteResult)result; Assert.That(result.RouteValues["controller"], Is.EqualTo("home")); Assert.That(result.RouteValues["action"], Is.EqualTo("index"));} Notice the duplication? When the tests are all starting to look like this, your spidey-sense should be tingling. The DRY principle (Don't Repeat Yourself) applies to your unit tests as well!
Let's see if we can do better. I'm just going to type some code that I wish I had:
controller.List().ShouldRenderDefaultView();
That's a lot more concise! It reads very well, and can easily eliminate 3-4 lines of repetitive test code. With .NET 3.5, we were given Extension Methods. If you aren't yet aware, extension methods allow us to bolt-on methods onto existing types. In this case, we want to add a method to the return value of our List() action, which is ActionResult.
I created a static class called TestHelperExtensions.cs, and in it, placed our new method:
public static void ShouldRenderDefaultView(this ActionResult result){ Assert.That(result, Is.TypeOf(ViewResult)); Assert.That(((ViewResult)result).ViewName, Is.EqualTo(string.Empty));}That's it? That was easy. Let's extend this to support rendering a view of any name....
public static void ShouldRenderView(this ActionResult result, string viewName){ Assert.That(result, Is.TypeOf(ViewResult)); Assert.That(((ViewResult)result).ViewName, Is.EqualTo(viewName));}public static void ShouldRenderDefaultView(this ActionResult result){ ShouldRenderView(result, string.Empty);}We can extend this idea further and capture the redirect family of asserts into another extension method:
controller.Login(username, password).ShouldRedirectTo("home", "index");//we should also support custom route valuesproductController.Save(...).ShouldRedirectTo("products", "show").WithRouteValue("id", 5);This can be implemented similarly...
//supports just passing in an actionpublic static RedirectToRouteResult ShouldRedirectTo(this ActionResult result, string action){ return ShouldRedirectTo(result, null, action);}public static RedirectToRouteResult ShouldRedirectTo(this ActionResult result, string controller, string action){ Assert.That(result, Is.TypeOf(typeof(RedirectToRouteResult)), "Should have redirected"); var redirectResult = (RedirectToRouteResult) result; WithRouteValue(redirectResult, "Controller", controller); WithRouteValue(redirectResult, "Action", action); return redirectResult;}public static RedirectToRouteResult WithRouteValue(this RedirectToRouteResult result, string key, object value){ Assert.That(result.Values[key], Is.EqualTo(value)); return result;}This supports basic redirects with just an action, an action and a controller, and arbitrary route values that we need to test as well.
Leveraging simple extension methods to make your tests more concise and readable can really help reduce the overall weight of your tests (especially if you want to change behavior later). This stands as an excellent reminder to write high-quality test code, just as you would for production code.


Jeremy Skinner
9.10.2008
1:08 PM
I added some very similar extension methods to MvcContrib a while back: www.jeremyskinner.co.uk/.../testing-action-