C# 对方法参数的BindAttribute进行单元测试

C# 对方法参数的BindAttribute进行单元测试,c#,asp.net-mvc,unit-testing,model-binding,C#,Asp.net Mvc,Unit Testing,Model Binding,我希望编写单元测试来验证我的控制器,同时确保绑定属性设置正确。使用下面的方法结构,我如何确保只有有效字段从单元测试中通过 public ActionResult AddItem([Bind(Include = "ID, Name, Foo, Bar")] ItemViewModel itemData) { if (ModelState.IsValid) { // Save and redirect } // Set Error Messages

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

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);
}
public ActionResult AddItem([Bind(Include=“ID,Name,Foo,Bar”)]ItemViewModel itemData)
{
if(ModelState.IsValid)
{
//保存和重定向
}
//设置错误消息
//重建对象下拉列表等。
itemData.AllowedFooValues=新列表();
返回视图(itemData);
}
更广泛的解释: 我们的许多模型都有不希望来回发送的允许值列表,因此当(ModelState.IsValid==false)出现错误时,我们会重新生成它们。为了确保所有这些都能正常工作,我们希望将单元测试放在适当的位置,以断言列表已重建,但在调用方法之前,如果不清除列表,测试将无效

我们正在使用本文中的helper方法来确保模型得到验证,然后我们的单元测试是这样的

    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);
    }
public void MyTest()
{
MyController控制器=新的MyController();
ActionResult=controller.AddItem();
Assert.IsNotNull(结果);
ViewResult ViewResult=结果为ViewResult;
Assert.IsNotNull(viewResult);
ItemViewModel itemData=viewResult.Model作为ItemViewModel;
Assert.IsNotNull(配方);
//验证模型,将由于空名称而失败
controller.ValidateViewModel(itemData);
//调用控制器操作
结果=控制器.AddItem(itemData);
Assert.IsNotNull(结果);
viewResult=结果为viewResult;
Assert.IsNotNull(viewResult);
itemData=viewResult.Model作为ItemViewModel;
//确保重新生成列表
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);
[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);
}
本质上,它所做的是迭代传递给给定控制器方法的每个参数,寻找绑定属性。如果它找到一个bind属性,那么它将获得
Include
列表,然后它将重置
viewModel
中未在Include列表中提及的所有属性(基本上解除绑定)

上面的代码可能需要一些调整,我没有做太多的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);
            }
        }
    }
}
publicstaticvoidprebindmodel(此TController控制器、TViewModel视图模型、字符串操作名){
foreach(typeof(TController).GetMethod(operationName).GetParameters()中的var paramtAction){
foreach(paramtAction.GetCustomAttributes(true)中的BindAttribute BindAttribute){/.Where(x=>x.AttributeType==typeof(BindAttribute))){
var propertiesToReset=typeof(TViewModel).GetProperties()。其中(x=>bindAttribute.IsPropertyAllowed(x.Name)==false);
foreach(propertiesToReset中的var propertyToReset){
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;
            }
        }
    }
仅供参考,我的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);
        }
    }
publicstaticvoidvalidateviewmodel(BaseAuthorizedController控制器,ViewModelBase viewmodeltovidate)
{
var validationContext=新的validationContext(viewModelToValidate,null,null);
var validationResults=新列表();
TryValidateObject(viewModelToValidate,validationContext,validationResults,true);
foreach(validationResults中的var validationResult)
{
controller.ModelState.AddModelError(validationResult.MemberNames.FirstOrDefault()??string.Empty,validationResult.ErrorMessage);
}
}

不清楚您在寻找什么。您是否在寻找一种方法来检测绑定属性是否已被使用,并在控制器上使用正确的值(ID、Foo…)进行设置?或者您正在寻找一种方法来测试MVC运行时是否正确地使用了该属性?或者寻找一种方法来手动将该属性应用于您的测试模型以重新创建MVC运行时的行为,以便您可以测试您的方法?或者完全其他的方法?我正在寻找一种方法来测试绑定属性是否被应用,以便如果模型具有fiel未声明绑定的ds,值从测试传递到控制器的方式与在post上从视图传递到控制器的方式不同。最终目标是确保所有字段都正确绑定,只有绑定值在post上更新到控制器,并且能够将“坏”模型(发生在某些服务器验证案例中)并通过测试执行if(ModelState.IsValid)的逻辑。很好,你能做到这一点。一般来说,将你的解决方案编辑到你的问题中不是一个好主意,因为它是有效的