johnny 5
johnny 5

Reputation: 20987

Testing Web Api Model Binders

I have a Web Api Model Binder which looks like so:

public class KeyModelBinder : IModelBinder
{
    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
    {  
     //...
    }
}

I'm trying to write a rule to make it easier to test. I've found a function that works with MVC's Model Binders here:

But when attempting to convert to use webApi I cannot figure out how to populate the value provider

    public TModel BindModel<TBinder, TModel>(NameValueCollection formCollection, TBinder binder)
        where TBinder : IModelBinder
    {
        var valueProvider = new NameValueCollectionValueProvider(formCollection, null);

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

        var bindingContext = new ModelBindingContext
        {
            ModelName = typeof(TModel).Name,
            ValueProvider = valueProvider,
            ModelMetadata = modelMetadata
        };

        binder.BindModel(null, bindingContext);
        return (TModel)bindingContext.ModelMetadata.Model;
    }

NameValueCollection Only Exist in MVC, How do I create a value provider for Web-Api

Upvotes: 1

Views: 842

Answers (2)

Reginald Blue
Reginald Blue

Reputation: 982

johnny 5's answer is correct, but, at least for me, it was hard to understand how to use it... so... consider the answer to this question which introduces a binder that we would like to unit test:

Passing UTC DateTime to Web API HttpGet Method results in local time

This merely converts all date times to a DateTimeKind.Utc. To unit test it, we need some fake URI (does not have to be in any way real):

    const string MOCK_URL = "http://localhost/api/george";

and then a unit test using his BindModelFromGet method, like so:

    [Test]
    public void should_convert_datetime_to_utc()
    {
        var bar = new UtcDateTimeModelBinder();
        var dateTime = BindModelFromGet<UtcDateTimeModelBinder, DateTime>
            ("fred", "?fred=2019-08-12 00:00:00Z", bar);
        Assert.That(dateTime.Kind, Is.EqualTo(DateTimeKind.Utc));
    }

Things to note:

  • The TBinder type parameter is type type of the binder to test.
  • The TModel type parameter is the expected type of your object.
  • The modelName parameter is the name of your query string parameter (can be anything).
  • The queryString is a made up query string that includes the parameter specified by modelName. It must match!
  • The binder parameter is an instance of your binder.

Additionally, if you are interested in testing error conditions (you should be), change the last line of his function to:

     throw new Exception(bindingContext.ModelState[modelName].Errors[0].ErrorMessage);

and test like this:

    [Test]
    public void should_handle_bad_dates()
    {
        var bar = new UtcDateTimeModelBinder();
        var ex = Assert.Throws<Exception>(() => BindModelFromGet<UtcDateTimeModelBinder, DateTime>
            ("fred", "?fred=NotADate", bar));
        Assert.That(ex.Message, Is.EqualTo("Cannot convert value to Utc DateTime"));
    }

That will handle simple validation problems, but do note that if the binder adds multiple errors or generally doesn't work like the simple date converter, you'll need to do more work.

Upvotes: 2

johnny 5
johnny 5

Reputation: 20987

It doesn't make to test model binders with out using the default value providers. So I wrote my bind model based on the expected rule. In this case I only needed to test gets

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

If you want this to work for post you take in a string of jsonValues modify the httpControllerContext like so:

httpControllerContext.Request = new HttpRequestMessage(HttpMethod.Post, "");
httpControllerContext.Request.Content = new ObjectContent<object>(jsonValues, new JsonMediaTypeFormatter());

then you just need to use the proper ValueProvider (I didn't do the research on how since it's unneeded for me).

Upvotes: 2

Related Questions