For the majority of cases the default model binder in MVC does all that we need it to do. Other times we just need to get stuck in and write our own and the process is largely painless.
Testing the Model Binders that we write, on the other hand, whilst not complicated in the traditional sense of complex algorithms and abstract thinking, is complex in that other way.
Ok, so complex probably isn’t the perfect word. I need another word that defines complexity in terms of the availability of information on a given topic and the obvious nature of the problem being solved.
Two things we do know about testing our model binder is that we’ll need an instance of it, and a way of providing it with some test data to bind against.
The second part of that raises some interesting questions. If we’re going to use test data then we need somewhere to store it that our model binder can access it. This forces us to look at the implementation of our model binder.
Handily we have access to a ValueProvider as part of the ModelBindingContext. Whilst there are other ways of obtaining the data to be bound, such as the ControllerContext.HttpContext.Request object, life becomes a little easier when we use the ValueProvider on the BindingContext.
(I know this because I was initially getting form values from controllerContext.HttpContext.Request["MyField"] and after writing the first test, discovered tis probably wasn’t the best idea)
In our test we can start by working backwards given the knowledge we currently have. That is, we can create an instance of our model binder, see what methods are available to us to get things going and work backwards from there.
If our model binder implements IModelBinder and doesn’t inherit or implement anything else, we’ve got a pretty limited number of methods we can work with. The most notable of these is the BindModel() method. This is the bit that’s going to kick things off and do the work we most likely want to test. If you’re inheriting your model binder from DefaultModelBinder and making use of the BindProperty method, you’ll note that, since it’s a protected method, it won’t be available to test directly. That’s OK since BindModel is going to call BindProperty for each property that it encounters so we’re good.
BindModel wants us to give it a couple of parameters, a ControllerContext and a BindingContext.
Since in my own example I’m now getting the form data from the BindingContext.ValueProvider, we can assume that the controller context doesn’t need anything doing with it. We can either create a mock or instantiate a new one for use:
var controllerContext = new ControllerContext(); |
Creating the binding context is the bit that takes a little more thinking about and probably some googling.
Both BindModel and BindProperty take a ModelBindingContext (System.Web.Mvc.ModelBindingContext) which we can go ahead and create which now means we have build-able code.
var controllerContext = new ControllerContext();
var bindingContext = new ModelBindingContext();
var modelBinder = new CustomModelBinder();
var result = modelBinder.BindModel(controllerContext, bindingContext); |
At this point you’ll notice that there’s not a lot you can do with the result as it is and there’s no generic version of BindModel, so we’ll want to cast this to the type we’re binding to:
var result = (CustomModel)modelBinder.BindModel(controllerContext, bindingContext); |
Now we need to figure out how to tell our binding context what we’d like to try and bind and to what model type.
ModelBindingContext has two main properties that we’re interested in: ValueProvider and ModelMetaData. There’s also a third parameter, ModelName, but I’ll come back to that in a moment.
In production our ValueProvider is going to be the forms collection that was posted to the server. In our test we need to create our own value provider and give it some data.
NameValueCollectionValueProvider in System.Web.Mvc is one value provider we can use quite easily and, as you can infer from its name, it’s a value provider for a NameValueCollection.
We can use the NameValueCollection to mimic our form post. My custom model binder is looking for the values “DateOfBirth.Day”, “DateOfBirth.Month” and “DateOfBirth.Year” inside the ValueProvider so we can also do the same in our NameValueCollection:
var formCollection = new NameValueCollection
{
{ "DateOfBirth.Day", "08" },
{ "DateOfBirth.Month", "10" },
{ "DateOfBirth.Year", "1984" }
};
var valueProvider = new NameValueCollectionValueProvider(formCollection, null); |
The ModelMetadata property on the ModelBindingContext is basically the meta data for the model we’re binding to. If you’ve never needed to obtain model meta data before (as I hadn’t) you can do so using the GetMetadataForType method on the ModelMetadataProviders class as follows:
var modelMetaData = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(CustomModel)); |
The ModelName property on ModelBindingContext needs to be either string.Empty or the name of the model being bound to.
So we now have all of the pieces we need to test our model binder. Putting this together into a more generic and reusable method looks like this:
TModel SetupAndBind(NameValueCollection nameValueCollection) where TBinder : IModelBinder, new() where TModel : class
{
var valueProvider = new NameValueCollectionValueProvider(nameValueCollection, null);
var modelMetaData = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(TModel));
var controllerContext = new ControllerContext();
var bindingContext = new ModelBindingContext
{
ModelName = string.Empty,
ValueProvider = valueProvider,
ModelMetadata = modelMetaData,
};
var modelBinder = new TBinder();
return (TModel)modelBinder.BindModel(controllerContext, bindingContext);
} |
Which can then be used like:
var formCollection = new NameValueCollection
{
{ "DateOfBirth.Day", "08" },
{ "DateOfBirth.Month", "10" },
{ "DateOfBirth.Year", "1984" }
};
var boundModel = this.SetupAndBind(formCollection);
Assert.AreEqual(DateTime.Parse("08/10/1984"), boundModel .DateOfBirth); |