Art Of Programming

musings by Dmytrii Nagirniak

Unit Test Actions With ValueProviderFactories in ASP.NET MVC3

The problem: In the process of upgrading from MVC2 to MVC3 we have faced an issue where some of the ValueProviders gain explicit access to static HttpContext thus making it too hard to test complex actions.

The solution was not obvious at first and I want to provide enough context before the solution. The way I test the controllers is this:

 

 

[TestFixture]
public class CurrentUserControllerTest : BaseTestFixture {
    private CurrentUserController controller;
    private Mock<ICurrentUserService> userService;
    private FakeHttpContext http;
    protected override void Init() {
        userService = new Mock<ICurrentUserService>();
        controller = new CurrentUserController(Services.Account, userService.Object)
            .FakeContext(context => http = context);
    }
    [Test]
    public void CanSeeSecuritySettingsPage() {
        userService.Setup(x => x.ViewUser()).Returns(new User {SecurityQuestion = "sq"});
        controller.SecuritySettings()
            .ShouldHaveModel<CurrentUserController.SecuritySettingsInfo>()
            .User.SecurityQuestion.Should().Be("sq");
    }
    [Test]
    public void SuccesfulUpdateRedirects() {
        controller.UpdateSecuritySettings()
            .ShouldRedirectTo(action: "Index", controller: "Dashboard");
    }
}

The most relevant piece of code here is the extension method FakeContext. It ensures the controller is not going to access real HttpContext. The main difference between MVC2 and 3 is that the latter uses HttpContext in FormValueProviderFactory and QueryStringValueProviderFactory. So we need to get rid of it. And of course we don’t want to modify all our tests and complicate them providing explicitly FormCollection. Additionally we definitely do not want to go into trouble instantiating HttpContext.

Fortunately MVC is extensible enough and has number of hook that we can use. In this case we need to replace those two bustards (FormValueProviderFactory and QueryStringValueProviderFactory). Which can be done with this simple extension method:

public static class ValueProviderFactoresExtensions {
    public static ValueProviderFactoryCollection ReplaceWith<TOriginal>(this ValueProviderFactoryCollection factories, Func<ControllerContext, NameValueCollection> sourceAccessor) {
        var original = factories.FirstOrDefault(x => typeof(TOriginal) == x.GetType());
        if (original != null) {
            var index = factories.IndexOf(original);
            factories[index] = new TestValueProviderFactory(sourceAccessor);
        }
        return factories;
    }
    class TestValueProviderFactory : ValueProviderFactory {
        private readonly Func<ControllerContext, NameValueCollection> sourceAccessor;
        public TestValueProviderFactory(Func<ControllerContext, NameValueCollection> sourceAccessor) {
            this.sourceAccessor = sourceAccessor;
        }
        public override IValueProvider GetValueProvider(ControllerContext controllerContext) {
            return new NameValueCollectionValueProvider(sourceAccessor(controllerContext), CultureInfo.CurrentCulture);
        }
    }
}

 

And the final implementation of FakeContext extension that I use (you will need to trim it to your particular case) is:

 

public static TController FakeContext<TController>(this TController controller, Action<FakeHttpContext> exposeAction = null) where TController: Controller {
    ApplicationMetaData.RegisterAll(); // Additionally register all ModelBinder so tests behave is in production
    if (ViewEngines.Engines.Where(x => x is SparkViewEngine).Empty()) {
        // We only use Spar view engine that can render view during testing
        ViewEngines.Engines.Clear();
        ViewEngines.Engines.Insert(0, GetTestViewFactory());
    }            
    // That is the entry to all the fakes, implementation is trivial so not here
    var context = new FakeHttpContext();
    controller.ControllerContext = new ControllerContext(context, new RouteData(), controller);
    controller.Url = new UrlHelper(new RequestContext(context, new RouteData()));
    // And finally, here we ensure no ValueProviders access HttpContext
    ValueProviderFactories.Factories
        .ReplaceWith<FormValueProviderFactory>(ctx => ctx.HttpContext.Request.Form))
        .ReplaceWith<QueryStringValueProviderFactory>(ctx => ctx.HttpContext.Request.QueryString));
    if (exposeAction != null)
        exposeAction.Invoke(context);
    return controller;
}        

 

Hope that helps. Have fun!

Comments

Dmytrii Nagirniak
I can't remember already, but there are couple of the ValueProviders that you need to replace. Just look at the exceptions you get. Should be pretty straight forward. Worked for all other people.
Anonymous
duzn't work

Comments