Art Of Programming

musings by Dmytrii Nagirniak

OnActionExecuting in ASP.NET MVC Unit Tests

I’m doing some initialisation in my application base controller class. Let’s say for our example that some of our controllers of our application need to have Language object defined. So that every page knows how to be localised.
Let’s also say we are not sure at this stage how we want to define the language. Possible options are:
  • App/Lang-1/Something/Else.aspx
  • App/Something/Else.aspx?lang=1
  • etc
But we just need that Language object now. So the things we need to do are:
  1. Add CurrentLanguage property to base controller.
  2. Retrieve CurrentLanguage using my GetObjectFromRrequest.
  3. Define the route(s).

No problems at all. All is done:
public abstract class LanguageEnabledController :  BaseAppController {

    private Language currentLanguage;

    public Language CurrentLanguage {
        get {
            if (currentLanguage == null)
                throw new InvalidOperationException("The language should be explicitly assigned. We don't want to provide a fallback.");
            return currentLanguage;
        }
    }


    public IWorkSpace WorkSpace {
        get {
            // Stripped...
        }
    }

    protected override void OnActionExecuting(ActionExecutingContext context) {
        base.OnActionExecuting(filterContext);
        currentLanguage = GetObjectFromRequest<Language>("language");
        if (currentLanguage == null) {
            // Invalid language object or missing URL
            context.Result = RedirectToAction("WrongLanguage", "Error", new { language = ValueProvider.GetValue("language") });
        }
    }


    public TObject GetObjectFromRequest<TObject>(string name) where TObject: class, ILoopBack {
        var vpr = ValueProvider.GetValue(name);
        if (vpr == null)
            return null;
        return WorkSpace.GetObject<TObject>(vpr.AttemptedValue);
    }

}

/*
Also add the route like this:
*/
routes.MapRoute(
    "DefaultWithLanguage",
    "Lang-{language}/{controller}/{action}.aspx",
    new { controller = "Home", action = "Index" }
    );

Ok. This works just great. Every controller now has CurrentLanguage property set correctly. If not, it will give us a message about that. So far so good.
But I spot a problem trying to test my class. Of course, the CurrentLanguage property is never set in tests because of OnActionExecuting is never called.
So what I need now is making this property accessible in tests. I see following options:
  1. Reward CurrentLanguage property with a setter.
  2. Inject a service into controllers’ constructor for that.
  3. Simulate OnActnioExecuting.
The easiest one is 1st, but we don’t look for an easy way. I don’t like this because of CurrentLanguage is really read-only. We’ll have test code in production. It will also avoid using Routes and binding which I prefer to stay and be part of the test (thou we don’t actuall test it).
The 2nd is probably the most correct one, but I don’t want to have service and implementation of it for a single property. I don’t want to use dependency injection in for now. And I don’t want to rewrite number of controller for that.
So I choose the 3rd one. Why is it bad? Because of we’ll have to simulate ControllerInvoker or some part of it. So let’s get to it.
But we have a problem with this approach: OnContextExecuting is PROTECTED method. We need to do following:
  1. Publish OnActionExecuting somehow to make it callable from tests.
  2. Simulate the Controllerinvoker.
The 2st step sounds like test-code in productino, but what it really is - just initialisation of the controller. Nothing wrong if it will be public (yes, publishing OnActionExecuting).
So let’s now put it all together and see the code.
Controller:
public class LanguageEnabledController :  BaseAppController {

    public IWorkSpace WorkSpace {
        get {
            // Stripped
        }
    }

    protected override void OnActionExecuting(ActionExecutingContext filterContext) {
        base.OnActionExecuting(filterContext);
        InitAppController(filterContext);
    }

    public virtual void InitAppController(ActionExecutingContext context) {
        currentLanguage = GetObjectFromRequest<Language>("language");
        if (currentLanguage == null) {
            // Invalid language object or missing URL
            context.Result = RedirectToAction("WrongLanguage", "Error", new { language = ValueProvider.GetValue("language") });
        }
    }


    public TObject GetObjectFromRequest<TObject>(string name) where TObject: class, ILoopBack {
        var vpr = ValueProvider.GetValue(name);
        if (vpr == null)
            return null;
        return WorkSpace.GetObject<TObject>(vpr.AttemptedValue);
    }

}

And the base test class:
public abstract class BaseControllerTest<TController> where TController : BaseAppController {

    [TestInitialize]
    public void TestInit() {
        SetUp();
    
        var methodParameterValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
        var preContext = new ActionExecutingContext(this.Controller.ControllerContext, methodParameterValues);
        Controller.InitAppController(preContext);            
    }
    
    protected virtual void SetUp() {           
    }
    
    
    private TController controller;
    public TController Controller {
        get {
            if (controller == null)
                controller = CreateTestController();
            return controller;
        }
    }
    private TController CreateTestController() {
        TController c = Activator.CreateInstance<TController>();
        PrepareTestController(c);
        return c;
    }
    
    
    IWorkSpace CreateNewSpace() {
        var ws = new UnitTestsWorkSpace();
        return ws;
    }
    
    private void PrepareTestController(TController c) {
        c.ProviderOfWorkSpace = new LocalInstanceWorkSpaceProvider(() => CreateNewSpace());
        c.SetTestContext(); // Extension to set Fake HttpContext
    }
    
    protected IWorkSpace WorkSpace {
        get {
            return Controller.WorkSpace;
        }
    }
}

Now we can easily execute tests like in this example:
[TestClass]
public class TestBusinessObjectController : BaseControllerTest<TopicController> {

    Language defaultLanguage;
    Language french;

    protected override void SetUp() {
        french = new Language(WorkSpace) {
            Code = "fr-FR",
            Name = "French"
        };
        defaultLanguage = new Language(WorkSpace) {
            Code = "en-AU"
            Name = "Aussie English"
        }
        WorkSpace.UpdateDatabase();
        
        // We need to put the ID of the language into any place that ValueProvider searches in...
        Controller.RouteData.Values["language"] = defaultLanguage.ExternalId(); // Extension to get an ID
    }


    [TestMethod]
    public void SelfCheck_ControllerHasLanguageSet() {
        Assert.IsNotNull(Controller.CurrentLanguage);
        Assert.AreEqual("en-AU", Controller.CurrentLanguage.Code);
    }

    [TestMethod]
    public void ListTopicsReturnsAussieTopic() {
        var expected = new Topic(WorkSpace) {
            Content = "Aussie"
            Language = defaultLanguage
        };
        new Topic(WorkSpace) {
            Content = "Who knows",
            Language = french
        }

        // We expect the Index returs all topics of current language only
        var topic = Controller.Index().ViewData.AsObject<IEnumerable<Topic>>("data").First();
        Assert.AreEqual(expected, topic);
    }
}

The keys are:

Enjoy!

Comments