4

I found the following post which describes how to test a custom model binder:

https://stackoverflow.com/a/55387164/155899

However, this tests an individual model binder. Ideally I'd like to be able to register all of my binders and test that the appropriate model was bound. This is how I would've achieved what I'm looking for in ASP.NET MVC:

// Register the model binders
ModelBinders.Binders[typeof(DateTime)] = new DateTimeModelBinder();
...

var values = new NameValueCollection {
    { "Foo", "1964/12/02 12:00:00" }
};

var controllerContext = CreateControllerContext(); // Utility method
var bindingContext = new ModelBindingContext() {
    ModelName = "Foo",
    ValueProvider = new NameValueCollectionValueProvider(values, null)
};
var binder = ModelBinders.Binders.GetBinder(typeof(DateTime));

var result = (DateTime)binder.BindModel(controllerContext, bindingContext);

Not only does this allow me to test the result of my model binder but it also makes sure the correct model binder is selected.

I'd appreciate it if someone could help. Thanks

nfplee
  • 7,643
  • 12
  • 63
  • 124
  • 2
    Note that if you test that the correct model binder is selected, you're testing Microsoft's code. That's fine, but a little redundant. [You can see how they test model binders in the code](https://github.com/aspnet/AspNetCore/blob/c565386a3ed135560bc2e9017aa54a950b4e35dd/src/Mvc/test/Mvc.IntegrationTests/BinderTypeBasedModelBinderIntegrationTest.cs). – Heretic Monkey May 03 '19 at 19:50
  • 1
    The reason I need to do this is because as far as I can tell, the framework will try to select the first model binder which is supported for the model type. Therefore if I did not register my model binder, it was registered in the wrong order or I build a newer model binder which accidentally overrides it then I need to make sure this fails. – nfplee May 03 '19 at 20:10
  • 1
    An integration test using test server would perhaps work better for you then – Vidmantas Blazevicius May 03 '19 at 20:12
  • @HereticMonkey - thanks, I’ve been playing around with this idea. However I had to copy so many different classes and utility methods that I gave up as it’s become a mess. – nfplee May 04 '19 at 09:36
  • @VidmantasBlazevicius thanks, I did investigate writing an integration test (using a test server) however from my understanding I would have to expose an end point with my model as a parameter. Please correct me if I’m wrong. – nfplee May 04 '19 at 09:37
  • You are correct, but i assumed you already have an endpoint so just call that – Vidmantas Blazevicius May 04 '19 at 09:54
  • @VidmantasBlazevicius see the link Heretic Monkey provided. This looks exactly what I wanted to do. From the looks of it, it creates a ParameterBinder which you can pass a ParameterDescriptor and controller context too. However this calls ModelBindingTestHelper, ModelBindingTestContext, DefaultObjectValidator (internal), TestModelValidatorProvider, DefaultModelValidatorProvider (internal), DataAnnotationsModelValidatorProvider (internal)... This is the point I gave up as there must be a simpler way. – nfplee May 04 '19 at 10:47

1 Answers1

5

I've managed to put something together. Thanks to @HereticMonkey for the link which helped me out.

[Fact]
public async Task Test() {
    // Arrange
    var services = new ServiceCollection();
    services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
    services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
    services.AddMvc(o => {
        o.ModelBinderProviders.Insert(0, new DateTimeModelBinderProvider());
    });
    var serviceProvider = services.BuildServiceProvider();

    var options = serviceProvider.GetService<IOptions<MvcOptions>>();
    var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(new List<IMetadataDetailsProvider>());
    var metadataProvider = new DefaultModelMetadataProvider(compositeDetailsProvider);
    var modelBinderFactory = new ModelBinderFactory(metadataProvider, options, serviceProvider);

    var parameterBinder = new ParameterBinder(
        metadataProvider,
        modelBinderFactory,
        new Mock<IObjectModelValidator>().Object,
        options,
        NullLoggerFactory.Instance);

    var parameter = new ParameterDescriptor() {
        Name = "parameter",
        ParameterType = typeof(DateTime)
    };

    var controllerContext = new ControllerContext() {
        HttpContext = new DefaultHttpContext() {
            RequestServices = serviceProvider // You must set this otherwise BinderTypeModelBinder will not resolve the specified type
        },
        RouteData = new RouteData()
    };

    var modelMetadata = metadataProvider.GetMetadataForType(parameter.ParameterType);

    var formCollection = new FormCollection(new Dictionary<string, StringValues>() {
        { "Foo", new StringValues("1964/12/02 12:00:00") }
    });
    var valueProvider = new FormValueProvider(BindingSource.Form, formCollection, CultureInfo.CurrentCulture);

    var modelBinder = modelBinderFactory.CreateBinder(new ModelBinderFactoryContext() {
        BindingInfo = parameter.BindingInfo,
        Metadata = modelMetadata,
        CacheToken = parameter
    });

    // Act
    var modelBindingResult = await parameterBinder.BindModelAsync(
        controllerContext,
        modelBinder,
        valueProvider,
        parameter,
        modelMetadata,
        value: null);

    // Assert
    Assert.True(modelBindingResult.IsModelSet);
}
nfplee
  • 7,643
  • 12
  • 63
  • 124