52

How can I test Controller.ViewData.ModelState? I would prefer to do it without any mock framework.

Bill the Lizard
  • 398,270
  • 210
  • 566
  • 880
eKek0
  • 23,005
  • 25
  • 91
  • 119

6 Answers6

45

You don't have to use a Mock if you're using the Repository Pattern for your data, of course.

Some examples: http://www.singingeels.com/Articles/Test_Driven_Development_with_ASPNET_MVC.aspx

// Test for required "FirstName".
   controller.ViewData.ModelState.Clear();

   newCustomer = new Customer
   {
       FirstName = "",
       LastName = "Smith",
       Zip = "34275",    
   };

   controller.Create(newCustomer);

   // Make sure that our validation found the error!
   Assert.IsTrue(controller.ViewData.ModelState.Count == 1, 
                 "FirstName must be required.");
SteveC
  • 15,808
  • 23
  • 102
  • 173
Scott Hanselman
  • 17,712
  • 6
  • 74
  • 89
  • 1
    A virtual modifier on Errors would be nice, alas there's an arrange tax, which looks a trifle heavy to me. – Ed Blackburn Feb 09 '12 at 11:45
  • IMHO Better solution is to use mvc conveyor. In this way you get more realistic behavior of your controller, you should deliver model validation to it's destiny - attribute validations. Below post is describing this (http://stackoverflow.com/a/5580363/572612). – Vladimir Shmidt Aug 03 '12 at 07:11
  • 1
    the article linked by Scott, seems tobe broken, can it be found else where? – Brian H May 15 '20 at 00:33
38
//[Required]
//public string Name { get; set; }
//[Required]
//public string Description { get; set; }

ProductModelEdit model = new ProductModelEdit() ;
//Init ModelState
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 );
ProductController.ModelState.Clear();
ProductController.ModelState.Merge(modelBinder.ModelState);

ViewResult result = (ViewResult)ProductController.CreateProduct(null,model);
Assert.IsTrue(result.ViewData.ModelState["Name"].Errors.Count > 0);
Assert.True(result.ViewData.ModelState["Description"].Errors.Count > 0);
Assert.True(!result.ViewData.ModelState.IsValid);
SteveC
  • 15,808
  • 23
  • 102
  • 173
VaSSaV
  • 381
  • 3
  • 2
  • I really like this method. As you state, it is far better at testing a model properly that has been decorated with validation attributes. – stevethethread Feb 28 '13 at 15:01
  • Very handy. I think I generally agree with folks that say that you shouldn't be testing your model properties, as that's a part of the framework, but there are times when it's critical to confirm for all future time that your controller is catching critical errors. – Ken Smith Dec 12 '13 at 19:22
  • Trying to find equivalent code for Web API. If anyone knows, perhaps they could update here? – Kieren Johnstone Jun 06 '15 at 09:28
  • @KierenJohnstone I've added an answer for Web API. – Bart Verkoeijen Jul 10 '16 at 06:13
  • Liked your response. You can create an extension method `public static void BindModelToController(this Controller controller, T model) { ... }` – Razvan Sep 12 '16 at 12:09
12

For testing Web API, use the Validate method on the controller:

var controller = new MyController();
controller.Configuration = new HttpConfiguration();
var model = new MyModel();

controller.Validate(model);
var result = controller.MyMethod(model);
Bart Verkoeijen
  • 16,545
  • 7
  • 52
  • 56
11

Ran into this problem for .NetCore 2.1 Here's my solution:

Extension Method

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;

namespace MyExtension
{
    public static void BindViewModel<T>(this Controller controller, T model)
    {
        if (model == null) return;

        var context = new ValidationContext(model, null, null);
        var results = new List<ValidationResult>();

        if (!Validator.TryValidateObject(model, context, results, true))
        {
            controller.ModelState.Clear();
            foreach (ValidationResult result in results)
            {
                var key = result.MemberNames.FirstOrDefault() ?? "";
                controller.ModelState.AddModelError(key, result.ErrorMessage);
            }
        }
    }
}

View Model

public class MyViewModel
{
    [Required]
    public string Name { get; set; }
}

Unit Test

public async void MyUnitTest()
{
    // helper method to create instance of the Controller
    var controller = this.CreateController();

    var model = new MyViewModel
    {
        Name = null
    };

    // here we call the extension method to validate the model
    // and set the errors to the Controller's ModelState
    controller.BindViewModel(model);

    var result = await controller.ActionName(model);

    Assert.NotNull(result);
    var viewResult = Assert.IsType<BadRequestObjectResult>(result);
}
  • 1
    THIS! Why doesn't DNC2 work like this out of the box :( All I wanted was to make a unit test making sure that `If !ModelState.IsValid, return BadRequest()` in a controller action – lozzajp Jun 03 '18 at 10:49
  • 1
    Works great unless the model is an IEnumerable. I hacked your code check if the model is an IEnumerable then to loop through each model doing the TryValidationObject check. – mobese46 Jun 22 '18 at 18:47
  • @mobese46 nice addition! – Paul - Soura Tech LLC Sep 01 '18 at 14:29
2

This not only let's you check that the error exists but also checks that it has the exact same error message as expected. For example both of these parameters are Required so their error message shows as "Required".

Model markup:

//[Required]
//public string Name { get; set; }
//[Required]
//public string Description { get; set; }

Unit test code:

ProductModelEdit model = new ProductModelEdit() ;
//Init ModelState
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 );
ProductController.ModelState.Clear();
ProductController.ModelState.Merge(modelBinder.ModelState);

ViewResult result = (ViewResult)ProductController.CreateProduct(null,model);
Assert.IsTrue(!result.ViewData.ModelState.IsValid);
//Make sure Name has correct errors
Assert.IsTrue(result.ViewData.ModelState["Name"].Errors.Count > 0);
Assert.AreEqual(result.ViewData.ModelState["Name"].Errors[0].ErrorMessage, "Required");
//Make sure Description has correct errors
Assert.IsTrue(result.ViewData.ModelState["Description"].Errors.Count > 0);
Assert.AreEqual(result.ViewData.ModelState["Description"].Errors[0].ErrorMessage, "Required");
abovetempo
  • 140
  • 2
  • 9
0

Adding to the great answers above, check out this fantastic use of the protected TryValidateModel method within the Controller class.

Simply create a test class inheriting from controller and pass your model to the TryValidateModel method. Here's the link: http://blog.icanmakethiswork.io/2013/03/unit-testing-modelstate.html

Full credit goes to John Reilly and Marc Talary for this solution.

Alex Stephens
  • 3,017
  • 1
  • 36
  • 41