98

I'm checking ModelState.IsValid in my controller action method that creates an Employee like this:

[HttpPost]
public virtual ActionResult Create(EmployeeForm employeeForm)
{
    if (this.ModelState.IsValid)
    {
        IEmployee employee = this._uiFactoryInstance.Map(employeeForm);
        employee.Save();
    }

    // Etc.
}

I want to mock it in my unit test method using Moq Framework. I tried to mock it like this:

var modelState = new Mock<ModelStateDictionary>();
modelState.Setup(m => m.IsValid).Returns(true);

But this throws an exception in my unit test case. Can anyone help me out here?

Jeroen
  • 60,696
  • 40
  • 206
  • 339
Mazen
  • 1,487
  • 2
  • 13
  • 14

3 Answers3

157

You don't need to mock it. If you already have a controller you can add a model state error when initializing your test:

// arrange
_controllerUnderTest.ModelState.AddModelError("key", "error message");

// act
// Now call the controller action and it will 
// enter the (!ModelState.IsValid) condition
var actual = _controllerUnderTest.Index();
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • how do we set the ModelState.IsValid to hit the true case? ModelState does not have a setter, and hence we cannot do the following: _controllerUnderTest.ModelState.IsValid = true. Without that, it will not hit the employee – Karan Jul 23 '12 at 11:07
  • 4
    @Newton, it's true by default. You don't need to specify anything to hit the true case. If you want to hit the false case you simply add a modelstate error as shown in my answer. – Darin Dimitrov Jul 25 '12 at 07:55
  • IMHO Better solution is to use mvc conveyor. In this way you get more realistic behavior of your controller, you should deliver model validation to it's destiny - attribute validations. Below post is describing this (http://stackoverflow.com/a/5580363/572612) – Vladimir Shmidt Aug 03 '12 at 07:16
13

The only issue I have with the solution above is that it doesn't actually test the model if I set attributes. I setup my controller this way.

private HomeController GenerateController(object model)
    {
        HomeController controller = new HomeController()
        {
            RoleService = new MockRoleService(),
            MembershipService = new MockMembershipService()
        };
        MvcMockHelpers.SetFakeAuthenticatedControllerContext(controller);

        // bind errors modelstate to the controller
        var modelBinder = new ModelBindingContext()
        {
            ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
            ValueProvider = new NameValueCollectionValueProvider(new NameValueCollection(), CultureInfo.InvariantCulture)
        };
        var binder = new DefaultModelBinder().BindModel(new ControllerContext(), modelBinder);
        controller.ModelState.Clear();
        controller.ModelState.Merge(modelBinder.ModelState);
        return controller;
    }

The modelBinder object is the object that test the validity of the model. This way I can just set the values of the object and test it.

uadrive
  • 1,249
  • 14
  • 23
  • 1
    Very nice, this is exactly what I was looking for. I don't know how many people post to an old question like this but it had value you for me. Thanks. – W.Jackson Jul 07 '12 at 00:49
  • 3
    Isn't it better to test the model in isolation with something like this? http://stackoverflow.com/a/4331964/3198973 – RubberDuck Jan 06 '17 at 14:47
  • 2
    While this is a clever solution, I agree with @RubberDuck. For this to be an actual, isolated unit-test, the validation of the model should be its own test, while the testing of the controller should have its own tests. If the model changes to violate the ModelBinder validation, your controller test will fail, which is a false positive since the controller logic isn't broken. To test an invalid ModelStateDictionary, simply add a fake ModelState error for the ModelState.IsValid check to fail. – xDaevax Mar 10 '18 at 16:15
2

uadrive's answer took me part of the way, but there were still some gaps. Without any data in the input to new NameValueCollectionValueProvider(), the model binder will bind the controller to an empty model, not to the model object.

That's fine -- just serialise your model as a NameValueCollection, and then pass that into the NameValueCollectionValueProvider constructor. Well, not quite. Unfortunately, it didn't work in my case because my model contains a collection, and the NameValueCollectionValueProvider does not play nicely with collections.

The JsonValueProviderFactory comes to the rescue here, though. It can be used by the DefaultModelBinder as long as you specify a content type of "application/json" and pass your serialised JSON object into your request's input stream (Please note, because this input stream is a memory stream, it's OK to leave it undisposed, as a memory stream doesn't hold on to any external resources):

protected void BindModel<TModel>(Controller controller, TModel viewModel)
{
    var controllerContext = SetUpControllerContext(controller, viewModel);
    var bindingContext = new ModelBindingContext
    {
        ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => viewModel, typeof(TModel)),
        ValueProvider = new JsonValueProviderFactory().GetValueProvider(controllerContext)
    };

    new DefaultModelBinder().BindModel(controller.ControllerContext, bindingContext);
    controller.ModelState.Clear();
    controller.ModelState.Merge(bindingContext.ModelState);
}

private static ControllerContext SetUpControllerContext<TModel>(Controller controller, TModel viewModel)
{
    var controllerContext = A.Fake<ControllerContext>();
    controller.ControllerContext = controllerContext;
    var json = new JavaScriptSerializer().Serialize(viewModel);
    A.CallTo(() => controllerContext.Controller).Returns(controller);
    A.CallTo(() => controllerContext.HttpContext.Request.InputStream).Returns(new MemoryStream(Encoding.UTF8.GetBytes(json)));
    A.CallTo(() => controllerContext.HttpContext.Request.ContentType).Returns("application/json");
    return controllerContext;
}
Rob Lyndon
  • 12,089
  • 5
  • 49
  • 74