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


90

我正在检 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);

但这在我的单元测试用例中引发了异常。有谁可以帮我离开这里吗?

Answers:


142

您无需嘲笑它。如果您已有控制器,则可以在初始化测试时添加模型状态错误:

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

我们如何设置ModelState.IsValid打真实的情况?ModelState没有设置器,因此我们无法执行以下操作:_controllerUnderTest.ModelState.IsValid = true。没有这些,它就不会对员工造成打击
Karan 2012年

4
@Newton,默认情况下是这样。您无需指定任何内容即可击中真实案例。如果您想打假,您只需添加一个modelstate错误,如我的答案所示。
Darin Dimitrov

恕我直言,更好的解决方案是使用MVC输送机。通过这种方式,您可以获得控制器的更真实的行为,应该将模型验证交付给它的命运-属性验证。后下描述的是这个(stackoverflow.com/a/5580363/572612
弗拉基米尔Shmidt

13

我上面的解决方案唯一的问题是,如果我设置属性,它实际上不会测试模型。我这样设置控制器。

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

modelBinder对象是测试模型有效性的对象。这样,我可以只设置对象的值并对其进行测试。


1
非常好,这正是我想要的。我不知道有多少人发布这样的老问题,但这对我来说很有价值。谢谢。
W.Jackson

似乎仍是一个不错的解决方案,仍在2016年:)
Matt Matt

2
用这样的东西孤立地测试模型不是更好吗?stackoverflow.com/a/4331964/3198973
RubberDuck

2
虽然这是一个聪明的解决方案,但我同意@Ru​​bberDuck。为了使它成为一个实际的,独立的单元测试,模型的验证应该是其自己的测试,而控制器的测试应该具有其自己的测试。如果模型更改违反了ModelBinder验证,则控制器测试将失败,这是误报,因为控制器逻辑未中断。要测试无效的ModelStateDictionary,只需添加一个虚假的ModelState错误,以使ModelState.IsValid检查失败。
xDaevax

2

uadrive的回答使我有些困惑,但是仍然存在一些差距。在的输入中没有任何数据时new NameValueCollectionValueProvider(),模型绑定器会将控制器绑定到一个空模型,而不是该model对象。

很好-只需将模型序列化为NameValueCollection,然后将其传递给NameValueCollectionValueProvider构造函数即可。好吧,不完全是。不幸的是,在我的情况下,它不起作用,因为我的模型包含一个集合,并且NameValueCollectionValueProvider不能很好地与集合配合使用。

JsonValueProviderFactory来这里的营救,虽然。它可以通过使用DefaultModelBinder只要您指定的内容类型的"application/json“,并通过你的序列化JSON对象到您的请求的输入流(请注意,因为此输入流是一个内存流,这是确定要离开它未予处置,作为存储器流不占用任何外部资源):

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;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.