单元测试方法参数的BindAttribute

我正在编写单元测试来验证我的控制器,同时确保绑定属性设置正确。 通过以下方法结构,我如何确保只有有效的字段从单元测试中传递?

public ActionResult AddItem([Bind(Include = "ID, Name, Foo, Bar")] ItemViewModel itemData)
{
    if (ModelState.IsValid)
    {
        // Save and redirect
    }

    // Set Error Messages
    // Rebuild object drop downs, etc.
    itemData.AllowedFooValues = new List<Foo>();
    return View(itemData);
}

更广泛的说明:我们的许多模型都有允许值的列表,我们不想来回发送,所以我们在(ModelState.IsValid == false)时重建它们。 为了确保这些工作,我们希望进行单元测试以声明列表已重建,但在调用方法之前不清除列表,测试无效。

我们使用这个SO答案中的帮助方法来确保模型得到验证,然后我们的单元测试就是这样的。

    public void MyTest()
    {
        MyController controller = new MyController();

        ActionResult result = controller.AddItem();
        Assert.IsNotNull(result);
        ViewResult viewResult = result as ViewResult;
        Assert.IsNotNull(viewResult);
        ItemViewModel itemData = viewResult.Model as ItemViewModel;
        Assert.IsNotNull(recipe);
        // Validate model, will fail due to null name
        controller.ValidateViewModel<ItemViewModel, MyController>(itemData);

        // Call controller action
        result = controller.AddItem(itemData);
        Assert.IsNotNull(result);
        viewResult = result as ViewResult;
        Assert.IsNotNull(viewResult);
        itemData = viewResult.Model as ItemViewModel;
        // Ensure list was rebuilt
        Assert.IsNotNull(itemData.AllowedFooValues);
    }

任何协助或指针在正确的方向非常感谢。


我可能会误解你在说什么,但是这听起来像是你想要一些东西来确保你在测试中创建的模型在被传递到你的控制器之前被过滤,以便模拟MVC绑定并防止你意外地写入一个将信息传递给你的控制器的测试,这个测试永远不会被框架填充。

考虑到这一点,我假定你只对Include成员集的绑定属性感兴趣。 在这种情况下,你可以使用这样的东西:

public static void PreBindModel<TViewModel, TController>(this TController controller, 
                                                         TViewModel viewModel, 
                                                         string operationName) {
    foreach (var paramToAction in typeof(TController).GetMethod(operationName).GetParameters()) {
        foreach (var bindAttribute in paramToAction.CustomAttributes.Where(x => x.AttributeType == typeof(BindAttribute))) {
            string properties;
            try {
                properties = bindAttribute.NamedArguments.Where(x => x.MemberName == "Include").First().TypedValue.Value.ToString();
            }
            catch (InvalidOperationException) {
                continue;
            }
            var propertyNames = properties.Split(',');

            var propertiesToReset = typeof(TViewModel).GetProperties().Where(x => propertyNames.Contains(x.Name) == false);

            foreach (var propertyToReset in propertiesToReset) {
                propertyToReset.SetValue(viewModel, null);
            }
        }
    }
}

在你调用控制器动作之前,你的单元测试会调用它,如下所示:

controllerToTest.PreBindModel(model, "SomeMethod");
var result = controllerToTest.SomeMethod(model);

本质上,它所做的是遍历每个传递给给定控制器方法的参数,寻找绑定属性。 如果它找到一个绑定属性,那么它将获取Include列表,然后重置Include列表中未提及的所有viewModel属性(实质上解除绑定)。

上面的代码可能需要一些调整,我不做很多的MVC工作,所以我已经对属性和模型的使用做了一些假设。

上述代码的改进版本,它使用BindAttribute本身来进行过滤:

public static void PreBindModel<TViewModel, TController>(this TController controller, TViewModel viewModel, string operationName) {
    foreach (var paramToAction in typeof(TController).GetMethod(operationName).GetParameters()) {
        foreach (BindAttribute bindAttribute in paramToAction.GetCustomAttributes(true)) {//.Where(x => x.AttributeType == typeof(BindAttribute))) {
            var propertiesToReset = typeof(TViewModel).GetProperties().Where(x => bindAttribute.IsPropertyAllowed(x.Name) == false);

            foreach (var propertyToReset in propertiesToReset) {
                propertyToReset.SetValue(viewModel, null);
            }
        }
    }
}

根据Forsvarir提供的答案,我想出了这个作为我的最终实施。 我删除了泛型以减少每次使用时的输入,并将其放入我的测试的基类中。 我还必须为具有相同名称但参数不同的多个方法做额外的工作(例如:Get vs. Post),这是通过所有方法的循环而不是GetMethod解决的。

    public static void PreBindModel(Controller controller, ViewModelBase viewModel, string operationName)
    {
        MethodInfo[] methods = controller.GetType().GetMethods();
        foreach (MethodInfo currentMethod in methods)
        {
            if (currentMethod.Name.Equals(operationName))
            {
                bool foundParamAttribute = false;
                foreach (ParameterInfo paramToAction in currentMethod.GetParameters())
                {
                    object[] attributes = paramToAction.GetCustomAttributes(true);
                    foreach (object currentAttribute in attributes)
                    {
                        BindAttribute bindAttribute = currentAttribute as BindAttribute;
                        if (bindAttribute == null)
                            continue;

                        PropertyInfo[] allProperties = viewModel.GetType().GetProperties();
                        IEnumerable<PropertyInfo> propertiesToReset =
                            allProperties.Where(x => bindAttribute.IsPropertyAllowed(x.Name) == false);

                        foreach (PropertyInfo propertyToReset in propertiesToReset)
                        {
                            propertyToReset.SetValue(viewModel, null);
                        }

                        foundParamAttribute = true;
                    }
                }

                if (foundParamAttribute)
                    return;
            }
        }
    }

总体而言,这成为一个非常干净和简单的解决方案,所以现在我的测试看起来像这样:

[TestMethod]
public void MyTest()
{
    MyController controller = new MyController();

    ActionResult result = controller.MyAddMethod();
    Assert.IsNotNull(result);
    ViewResult viewResult = result as ViewResult;
    Assert.IsNotNull(viewResult);
    MyDataType myDataObject = viewResult.Model as MyDataType;
    Assert.IsNotNull(myDataObject);
    ValidateViewModel(myController, myDataObject);
    PreBindModel(controller, myDataObject, "MyAddMethod");
    Assert.IsNull(myDataObject.FieldThatShouldBeReset);
    result = controller.MyAddMethod(myDataObject);
    Assert.IsNotNull(result);
    viewResult = result as ViewResult;
    Assert.IsNotNull(viewResult);
    myDataObject = viewResult.Model as MyDataType;
    Assert.IsNotNull(myDataObject.FieldThatShouldBeReset);
}

仅供参考,我的ValidateViewModel方法是:

    public static void ValidateViewModel(BaseAuthorizedController controller, ViewModelBase viewModelToValidate)
    {
        var validationContext = new ValidationContext(viewModelToValidate, null, null);
        var validationResults = new List<ValidationResult>();
        Validator.TryValidateObject(viewModelToValidate, validationContext, validationResults, true);
        foreach (var validationResult in validationResults)
        {
            controller.ModelState.AddModelError(validationResult.MemberNames.FirstOrDefault() ?? string.Empty, validationResult.ErrorMessage);
        }
    }
链接地址: http://www.djcxy.com/p/26839.html

上一篇: Unit Test the BindAttribute for method parameters

下一篇: How to add individual objects to django haystack?