30

How do I unit test a custom ModelBinder?

Here's the code.

public class MagicBinder : DefaultModelBinder
    {

        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var boundModelObject = base.BindModel(controllerContext, bindingContext);

            var properties = bindingContext.ModelType.GetProperties().Where(a => a.CanWrite);
            foreach (var propertyInfo in properties)
            {
                object outValue = null;
                bindingContext.TryGetValue(propertyInfo.Name, propertyInfo.DeclaringType, out outValue);
                propertyInfo.SetValue(boundModelObject, outValue, null);
            }

            return boundModelObject;
        }
    }

And here is the test script.

[TestMethod]
public void TestFooBinding()
{
    var dict = new ValueProviderDictionary(null)
                   {
                       {"Number", new ValueProviderResult("2", "2", null)},
                       {"Test", new ValueProviderResult("12", "12", null)},
                   };

    var bindingContext = new ModelBindingContext() { ModelName = "foo", ValueProvider = dict};

    var target = new MagicBinder();

    Foo result = (Foo)target.BindModel(null, bindingContext);
}

public class Foo
{
    public int Number { get; set; }
    public int Test { get; set; }
}

Problem? In the MagicBinder, bindingContext.Model is null. If I try set it with bindingContext.Model = new Foo(). I get an exception saying it is deprecated, and I should set the ModelMetadata.

So how do I construct a ModelMetadata? It can't even be mocked.

Sleeper Smith
  • 3,212
  • 4
  • 28
  • 39
  • Just a note for future readers, TryGetValue is no longer available (post-MVC1): http://stackoverflow.com/questions/4149805/valueprovider-does-not-contain-a-definition-for-trygetvalue – fordareh Aug 06 '13 at 16:26
  • possible duplicate of [Unit testing custom model binder in ASP.NET MVC 2](http://stackoverflow.com/questions/1992629/unit-testing-custom-model-binder-in-asp-net-mvc-2) – Marijn Apr 08 '14 at 12:14

2 Answers2

46

NOTE: This answer is for ASP.NET on .NET Framework and might be outdated.

Try like this:

[TestMethod]
public void TestFooBinding()
{
    // arrange
    var formCollection = new NameValueCollection 
    {
        { "Number", "2" },
        { "Test", "12" },
    };

    var valueProvider = new NameValueCollectionValueProvider(formCollection, null);
    var metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(Foo));
    var bindingContext = new ModelBindingContext
    {
        ModelName = "",
        ValueProvider = valueProvider,
        ModelMetadata = metadata
    };
    var controllerContext = new ControllerContext();
    var sut = new MagicBinder();
        
    // act    
    Foo actual = (Foo)sut.BindModel(controllerContext, bindingContext);

    // assert
    // TODO:
}
Sedat Kapanoglu
  • 46,641
  • 25
  • 114
  • 148
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • Thanks a bunch. What is the reason for using ModelMetadata concrete class though? Why can't they just implement an interface as all it's doing is well providing "metadata" for a model type. – Sleeper Smith Feb 19 '11 at 03:43
  • 1
    just as an FYI for anyone stumbling across this: if you needed access to metadata.Model then you will have to set the accessor which is the first argument for GetMetadataForType and has to be a Func so can be as simple as `() => new MyModelClass()` – Ben Mar 27 '17 at 15:35
  • Not out dated for .NET Framework, but will not work in .NET Core – iGanja Mar 29 '21 at 16:24
3

Incase any of you need this to work for web-api you can use this method which will test's Get Requests, you get the benefit of using the built in provider:

Which will populate the values as the would come in from the web, instead of getting bizarre side effects of creating values that the provider may potentially never return Null etc.

using System;
using System.Globalization;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Metadata.Providers;
using System.Web.Http.ModelBinding;
using System.Web.Http.ValueProviders.Providers;

namespace Apps.API.Web.Tests
{
    public class ModelBinderTestRule
    {
        //This URL is just a place holder for prefixing the query string
        public const string MOCK_URL = "http://localhost:8088/";

        public TModel BindModelFromGet<TBinder, TModel>(string modelName, string queryString, TBinder binder)
            where TBinder : IModelBinder
        {
            var httpControllerContext = new HttpControllerContext();
            httpControllerContext.Request = new HttpRequestMessage(HttpMethod.Get, MOCK_URL + queryString);
            var bindingContext = new ModelBindingContext();

            var dataProvider = new DataAnnotationsModelMetadataProvider();
            var modelMetadata = dataProvider.GetMetadataForType(null, typeof(TModel));

            var httpActionContext = new HttpActionContext();
            httpActionContext.ControllerContext = httpControllerContext;

            var provider = new QueryStringValueProvider(httpActionContext, CultureInfo.InvariantCulture);

            bindingContext.ModelMetadata = modelMetadata;
            bindingContext.ValueProvider = provider;
            bindingContext.ModelName = modelName;

            if (binder.BindModel(httpActionContext, bindingContext))
            {
                return (TModel)bindingContext.Model;
            }

            throw new Exception("Model was not bindable");
        }
    }
}
johnny 5
  • 19,893
  • 50
  • 121
  • 195
  • Minor point: If you happen to be using model validation in your binder, this will crash. Adding "httpControllerContext.Configuration = new HttpConfiguration()" will prevent the crash. – Reginald Blue Feb 24 '20 at 18:13