【问题标题】:Unit Test the BindAttribute for method parameters对方法参数的 BindAttribute 进行单元测试
【发布时间】:2015-08-20 16:04:36
【问题描述】:

我希望编写单元测试来验证我的控制器,同时确保正确设置绑定属性。使用下面的方法结构,如何确保单元测试只通过有效字段?

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 answer 中的辅助方法来确保模型得到验证,然后我们的单元测试是这样的。

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

非常感谢任何正确方向的帮助或指示。

【问题讨论】:

  • 有点不清楚你在找什么。您是否正在寻找一种方法来检测绑定属性是否已被使用并在您的控制器(ID、Foo...)上设置正确的值?或者您是否正在寻找一种方法来测试 MVC 运行时是否正确使用该属性?或者手动将属性应用到您的测试模型以重新创建 MVC 运行时的行为,以便您可以测试您的方法?还是完全不同的东西?
  • 我正在寻找一种方法来测试是否应用了绑定属性,这样如果模型具有未声明绑定的字段,则值不会从测试传递到控制器这样它们就不会从视图传递到帖子上的控制器。最终目标是确保所有字段都正确绑定,只有绑定值在发布到控制器时更新,并且能够发布“坏”模型(发生在某些服务器验证案例中)并具有逻辑for if (ModelState.IsValid) 由测试执行。
  • 很高兴你能完成这项工作。将您的解决方案编辑到您的问题中通常不是一个好主意,因为它实际上是一个答案,而不是一个问题。我可以建议您从您的问题中编辑它并将其发布为问题的自我回答。它有助于划分问答,作为奖励,您最终可能会得到奇怪的支持。如果您想在人们查看帖子时将您的解决方案保留在问题旁边,那么您也可以将接受的答案切换到您的帖子(尽管其他人的观点不同,我对此没有问题)
  • 分离的好想法。我编辑添加了我的答案,但会让你的答案被接受,因为这是让我得到最终解决方案的原因。非常感谢您对此的帮助。

标签: c# asp.net-mvc unit-testing model-binding


【解决方案1】:

我可能误解了您的意思,但听起来您需要确保在将您在测试中创建的模型传递给控制器​​之前对其进行过滤,以模拟 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 列表,然后重置包含列表中未提及的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);
            }
        }
    }
}

【讨论】:

  • 这与我要找的很接近。我希望 MVC 中有一个用于测试的内置功能,可以让我触发实际的绑定代码而不是模拟它。最坏的情况,如果需要,我可以走这条路。
  • @MartinNoreke 我对代码进行了一些改进,以使用 BindAttribute 进行过滤,尽管它仍然假定模型已填充,然后删除不应该存在的值。我认为要更接近运行时,您可能会发现您必须开始使用控制器上下文和值提供程序实际调用绑定逻辑。如果你没有得到你想要的东西,那么你可能想要挖掘一下 MVC 源代码:aspnetwebstack.codeplex.com
  • 我将在一两天内再次开始深入研究。我完成了其他一些工作(这从未发生过:),但我确实认为这让我朝着正确的方向前进。
【解决方案2】:

根据 Forsvarir 提供的答案,我想出了这个作为我的最终实现。我删除了泛型以减少每次使用时的输入,并将其放在我的测试的基类中。我还必须为具有相同名称但参数不同的多个方法(例如:Get 与 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);
        }
    }

【讨论】:

    猜你喜欢
    • 2017-06-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-29
    • 2012-06-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多