Unit Test the BindAttribute for method parameters

I am looking to write unit tests to validate my controller while ensuring that the bind properties are setup correctly. With the following method structure, how can I ensure that only the valid fields are passed from a unit test?

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

Broader Explanation: Many of our models have lists of allowed values that we don't want to send back and forth, so we rebuild them when the (ModelState.IsValid == false). In order to ensure these all work, we want to put unit tests in place to assert that the list was rebuilt, but without clearing the list before calling the method, the test is invalid.

We are using the helper method from this SO answer for ensuring the model is validated, and then our unit test is something like this.

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

Any assistance or pointers in the right direction is greatly appreciated.


I may be misinterpreting what you're saying, but it sounds like you want something to ensure that a model you've created in your test is filtered before it is passed to your controller in order to simulate MVC binding and to prevent you accidentally writing a test that passes information to your controller under test that would never actually be populated by the framework.

With this in mind, I've assumed you're only really interested in Bind attributes with the Include member set. In which case you could use something like this:

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

Which as it stands would be called from your unit test, before you invoke the controller action like this:

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

Essentially, what it does is iterate through each of the parameters that are being passed to a given controller method, looking for bind attributes. If it finds a bind attribute, then it gets the Include list, then it resets every property of the viewModel that isn't mentioned in the include list (essentially unbinding it).

The above code may need some tweaking, I don't do much MVC work, so I've made some assumptions about the usage of the attribute and models.

An improved version of the above code, that uses the BindAttribute itself to do the filtering:

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

Based upon the answer provided by Forsvarir, I came up with this as my final implementation. I removed the generics to reduce typing each time it was used, and put this in a base class of my tests. I also had to do some extra work for multiple methods with the same name but different parameters (ex: Get vs. Post) which was solved by the loop of all methods instead of 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;
            }
        }
    }

Overall this became a very clean and easy solution, so now my tests look like this:

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

Just for reference, my ValidateViewModel method is:

    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/26840.html

上一篇: Spring事务注释

下一篇: 单元测试方法参数的BindAttribute