MVC: Unit Testing Action Filters

Certain parts of ASP.NET MVC can be a real pain to test. Namely Model Binders, Action Filters and anything that relies on some magic “Context” that seems to derive from HttpContext, ControllerContext, RequestContext, etc.

Below, I’ve outlined how I’ve unit tested a Action Filter. You could extrapolate some of this code into a base Test Class, but I’ve left it all in one place so you can see whats going on.

Background

The action filter filters out Employees based upon a “group”. Employee id’s are in a table and if the employee id has a parent id it is part of a group. I want to be able to put this action filter on any action or controller where groups should not be allowed. If the employee is part of a group, I want to change the ViewResult to ContentResult and stuff some html in there for the user to see. Therefore if the user cannot cannot access this screen, they’ll still see the master page, etc, but the view will be some plain text letting them know that they cannot view the page through a group.

Here’s the code:

using System.Web.Mvc;
using SharpArch.Core;

namespace Agilevent.UI.Web.ActionFilters
{
    public class EmployeeGroupRestrictedActionFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            // Get Employee Id
            var employeeId = int.Parse(filterContext.RouteData.GetRequiredString("employeeId"));
            var employeeInfo = SafeServiceLocator<IEmployeeInfoService>.GetService().GetById(employeeId);

            if (employeeInfo.IsGroup)
            {
                filterContext.Result = new ContentResult {Content = HtmlResultString};
            }

            base.OnActionExecuted(filterContext);
        }

        public string HtmlResultString
        {
            get { return "This screen is only accessible by selecting a the actual employee. You cannot view it through a group id."; }
        }

    }
}

The Unit Test

The unit test mocks out the Action Executed Context with the values that I need mocked out. I make sure that the RouteData contains a value that I need and then the Service Locator is set up to return a mock service which will return a mocked out Employee with a  known state. Since the employee is a group the test will fall into the ContentResult and we can test from there.

Here’s the code:

using System;
using System.Security.Policy;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Microsoft.Practices.ServiceLocation;
using Moq;
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;
using StructureMap;
using StructureMap.ServiceLocatorAdapter;

namespace Agilevent.UnitTests.MVC.ActionFilters
{
    [TestFixture]
    public class EmployeeGroupRestrictedActionFilterTests
    {
        private Container _container;
        private Mock<IEmployeeInfoService> _employeeInfoService;
        private EmployeeInfo _employeeInfo;

        [Test]
        public void should_not_be_able_to_continue_if_the_employee_id_is_a_group()
        {
            // Set up fake employee info as a group
            _employeeInfo = new EmployeeInfo();
            _employeeInfo.IsGroup = true;

            // Set up employee info service to return the fake employee when called with "123"
            _employeeInfoService = new Mock<IEmployeeInfoService>();
            _employeeInfoService.Setup(b => b.GetById(It.Is<int>(i => i == 123))).Returns(_employeeInfo);

            // Set up the container & Service Locator
            _container = new Container();
            _container.Configure(x => x.For<IEmployeeInfoService>().Use(_employeeInfoService.Object));
            ServiceLocator.SetLocatorProvider(() => new StructureMapServiceLocator(_container));

            // Mock out the context to run the action filter.
            var request = new Mock<HttpRequestBase>();
            var httpContext = new Mock<HttpContextBase>();
            httpContext.SetupGet(c => c.Request).Returns(request.Object);

            var routeData = new RouteData(); //
            routeData.Values.Add("employeeId", "123");

            var actionExecutedContext = new Mock<ActionExecutedContext>();
            actionExecutedContext.SetupGet(r => r.RouteData).Returns(routeData);
            actionExecutedContext.SetupGet(c => c.HttpContext).Returns(httpContext.Object);

            var filter = new EmployeeGroupRestrictedActionFilterAttribute();

            filter.OnActionExecuted(actionExecutedContext.Object);

            // Assert
            Assert.That(actionExecutedContext.Object.Result, Is.InstanceOfType(typeof(ContentResult)));
            Assert.That((actionExecutedContext.Object.Result as ContentResult).Content, Is.EqualTo(filter.HtmlResultString));
        }

    }
}

I hope that helps someone out there, I know I’ll probably even come back at some point in the future to see how I did it. :) Enjoy.

  • wesmcclure

    IMHO, if you have this many dependencies, you are not Unit Testing. A unit is typically isolated and coupled to it's dependencies. An action filter, by its AOP nature, is an integration component.

  • http://aspdotnetmvc.com/blogs/default.aspx ASP.NET MVC Archived Blog Posts, Page 1

    [...] to VoteMVC: Unit Testing Action Filters (4/21/2010)Wednesday, April 21, 2010 from blog.donnfelker.comCertain parts of ASP.NET MVC can be a real pain to [...]

  • http://www.esenciadev.com Ryan

    ActionFilters are a pain to test, and while it's not a unit test in the 'pure' sense, it's definitely better to have some kind of test around this functionality than none at all. We started moving all our MVC integration-ish tests to using the test helpers in the mvccontrib (http://mvccontrib.codeplex.com) project, and that really helped. Our tests are less brittle, and they're easier to write. We found our filters started having more and more dependencies in them as a result of the complexity of the app going up, and eventually found a good way to wrap up the DI of the Action Filters – http://www.esenciadev.com/2010/05/dependency-in…

  • Coach Factory Outlet
  • Anonymous

    football boots comfortable and cheap
    football shoes designer bags Come surprise
    nike air max ltd good  trend  cheap
    gucci sunglasses Do not miss the nike-low prices
    gucci outlet online discount designer shoes
    gucci outlet 2012 is the master of them
    wholesale coach The new discount
    air max shoes Business casual bag cheap
    coach purses on sale Boutique explosion models
    wholesale coach The new discount fashion
    cheap basketball jerseys See this beautiful bag
    wholesale gucci shoes Affordable surprise bag
    discount gucci online Come to see the lowest package
    cheap authentic nfl jerseys online Fashion accessories boutique