How to unit test modelbinder with ModelMetadata

2019-01-22 07:26发布

问题:

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.

回答1:

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:
}


回答2:

In cause 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");
        }
    }
}