Unit testing MVC3/MVC4 Model Binders

Update: I'm always trying to find improved ways of implementing and testing model binders. See https://github.com/MattDavies/MvcModelBinderTesting for the latest version, and feel free to send a pull request if you have any suggestions!

Here's a really simple helper base class for testing MVC model binders. There's an example at the bottom of this post covering usage of this class.

This will work for simple model binders which don't take IoC dependencies, use the BindingContext to retrieve values and don't use the ControllerContext parameter passed to BindModel():

public abstract class ModelBinderTestBase<TBinder, TModel> where TBinder : IModelBinder, new()
{
    private ModelBindingContext _bindingContext;
    private NameValueCollection _formCollection = new NameValueCollection();

    protected void SetFormValues(NameValueCollection formValues)
    {
        _formCollection = formValues;
    }

    protected TModel BindModel()
    {
        SetupBindingContext();
        new TBinder().BindModel(null, _bindingContext);
        return (TModel)_bindingContext.ModelMetadata.Model;
    }

    private void SetupBindingContext()
    {
        var valueProvider = new NameValueCollectionValueProvider(_formCollection, null);
        var modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(TBinder));
        _bindingContext = new ModelBindingContext
        {
            ModelName = typeof(TModel).Name,
            ValueProvider = valueProvider,
            ModelMetadata = modelMetadata
        };
    }
}

We can extend this base class to cover dependencies by resolving the model binder using your favourite mocking library. I use a library I helped create called AutofacContrib.NSubstitute:

public abstract class ModelBinderTestBase<TBinder, TModel> where TBinder : IModelBinder
{
    private ModelBindingContext _bindingContext;
    private NameValueCollection _formCollection = new NameValueCollection();

    protected void SetFormValues(NameValueCollection formValues)
    {
        _formCollection = formValues;
    }

    protected TModel BindModel()
    {
        SetupBindingContext();
        new AutoSubstitute().Resolve<TBinder>().BindModel(null, _bindingContext);
        return (TModel)_bindingContext.ModelMetadata.Model;
    }

    private void SetupBindingContext()
    {
        var valueProvider = new NameValueCollectionValueProvider(_formCollection, null);
        var modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(TBinder));
        _bindingContext = new ModelBindingContext
        {
            ModelName = typeof(TModel).Name,
            ValueProvider = valueProvider,
            ModelMetadata = modelMetadata
        };
    }
}

We can easily extend the class to support ControllerContext using the same mocking library.

Final version (Autofac / NSubstitute / NUnit) - feel free to adjust to suit your needs or leave a comment if you need a hand:

public abstract class ModelBinderTestBase<TBinder, TModel> where TBinder : DefaultModelBinder
{
    #region Setup base + private methods

    private ControllerContext _context;
    private ModelBindingContext _bindingContext;
    protected AutoSubstitute AutoSubstitute;
    private NameValueCollection _formCollection;

    [SetUp]
    protected void Setup()
    {
        AutoSubstitute = new AutoSubstitute();
        var httpContext = Substitute.For<HttpContextBase>();
        var request = Substitute.For<HttpRequestBase>();
        httpContext.Request.Returns(request);
        var controllerContext = Substitute.For<ControllerContext>();
        controllerContext.HttpContext = httpContext;
        AutoSubstitute.Provide(request);
        AutoSubstitute.Provide(httpContext);
        AutoSubstitute.Provide(controllerContext);

        _context = AutoSubstitute.Resolve<ControllerContext>();
    }

    private void SetupBindingContext()
    {
        _formCollection = _context.HttpContext.Request.Form;
        var valueProvider = new NameValueCollectionValueProvider(_formCollection, null);
        var modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(TBinder));
        _bindingContext = new ModelBindingContext
        {
            ModelName = typeof(TModel).Name,
            ValueProvider = valueProvider,
            ModelMetadata = modelMetadata
        };
    }

    #endregion

    [Test]
    public void Bind_to_correct_model()
    {
        var attr = (ModelBinderTypeAttribute)Attribute.GetCustomAttribute(typeof(TBinder), typeof(ModelBinderTypeAttribute));
        Assert.That(attr, Is.Not.Null, "The ModelBinderType attribute is missing");
        Assert.That(attr.TargetTypes, Is.Not.Null, "No bindings are defined");
        Assert.That(attr.TargetTypes.ToList()[0], Is.EqualTo(typeof(TModel)), String.Format("The binding is incorrect; it should be {0}", typeof(TModel).FullName));
    }

    protected void SetFormValues(NameValueCollection formValues)
    {
        _context.HttpContext.Request.Form.Returns(formValues);
    }

    protected TModel BindModel()
    {
        SetupBindingContext();
        AutoSubstitute.Resolve<TBinder>().BindModel(_context, _bindingContext);
        return (TModel)_bindingContext.ModelMetadata.Model;
    }

    protected void AssertModelError(string key, string error)
    {
        Assert.That(_bindingContext.ModelState.ContainsKey(key), key + " not present in model state");
        Assert.That(_bindingContext.ModelState[key].Errors.Count, Is.EqualTo(1), "Expecting an error against " + key);
        Assert.That(_bindingContext.ModelState[key].Errors[0].ErrorMessage, Is.EqualTo(error), "Expecting different error message for model state against " + key);
    }
}

Example model:

public class ExampleViewModel
{
    public int? TestInteger { get; set; } 
}

Example model binder:

[ModelBinderType(typeof(ExampleViewModel))]
public class ExampleModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var viewModel = new ExampleViewModel();

        var monkey = bindingContext.ValueProvider.GetValue("someNumber");
        if (monkey == null)
        {
            bindingContext.ModelState.AddModelError("TestInteger", "You didn't submit a number.");
        }
        else
        {
            viewModel.TestInteger = (int?) monkey.ConvertTo(typeof (int?));
        }

        bindingContext.ModelMetadata.Model = viewModel;
        return base.BindModel(controllerContext, bindingContext);
    }
}

Example test:

[TestFixture]
public class ExampleModelBinderShould : ModelBinderTestBase<ExampleModelBinder, ExampleViewModel>
{
     [Test]
     public void Have_int_set_to_input()
     {
         SetFormValues(new NameValueCollectionsomeNumber);

         var vm = BindModel();

         Assert.That(vm.TestInteger, Is.EqualTo(3));
     }

    [Test]
    public void Not_bind_if_no_integer_submitted()
    {
        var vm = BindModel();

        Assert.That(vm.TestInteger, Is.Null);
        AssertModelError("TestInteger", "You didn't submit a number.");
    }
}
Tweet