如何使用 Moq 框架模拟 ModelState.IsValid?

我在我的控制器操作方法中检查 ModelState.IsValid,它创建了一个像下面这样的 Employee:

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


// Etc.
}

我想在我的单元测试方法中使用 Moq Framework 来模拟它,我试着这样模拟它:

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

但是这在我的单元测试用例中抛出了一个异常?

23924 次浏览

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();

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'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;
}