25

When testing my controller's actions the ModelState is always valid.

public class Product
{
    public int Id { get; set; }

    [Required]
    [StringLength(10)]
    public string Name { get; set; }

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

    [Required]
    public decimal Price { get; set; }
}

And my controller.

public class ProductController : Controller
{
      [HttpPost]
      public ActionResult Create(Product product)
      {
            if (ModelState.IsValid)
            {
                   // Do some creating logic...
                   return RedirectToAction("Display");
            }

             return View(product);              
      }
 }

And test:

[Test]
public TestInvalidProduct()
{
     var product = new Product();
     var controller = new ProductController();
     controller.Create(product);
     //controller.ModelState.IsValid == true
}

Why the modelState is valid when the product doesn't have a name, Description and price?

gdoron
  • 147,333
  • 58
  • 291
  • 367
  • You are calling the controller action as a normal method, not going through the MVC 'stack' which would do model binding and validation. – Richard Dalton Nov 17 '11 at 09:54
  • 2
    @RichardD So how do I know the model state do the job in the action? I have to test it with Unit testing – gdoron Nov 17 '11 at 10:01
  • 1
    I don't know :D That's why I didn't post this as an answer. dskh's answer might help you. – Richard Dalton Nov 17 '11 at 10:03
  • 1
    possible duplicate of [Unit tests on MVC validation](http://stackoverflow.com/questions/1269713/unit-tests-on-mvc-validation) – Richard Dalton Nov 17 '11 at 10:05
  • 1
    If your controller's logic is dependent on the presence of any error, you should arrange the controller by adding a test error to its ModelState. The actual validation of your model should be tested separately. Of course if your controller is supposed to react on a specific error, simply add that one error to the ModelState – TiMoch Jun 06 '13 at 13:46

7 Answers7

18

Validation happens when the posted data is bound to the view model. The view model is then passed into the controller. You are skipping part 1 and passing a view model straight into a controller.

You can manually validate a view model using

System.ComponentModel.DataAnnotations.Validator.TryValidateObject()
abatishchev
  • 98,240
  • 88
  • 296
  • 433
Dan
  • 3,229
  • 2
  • 21
  • 34
  • 5
    Can you please show an example how you use the: DataAnnotations.Validator.TryValidateObject() to test the product in this case – gdoron Nov 17 '11 at 10:14
  • 1
    This post (from Richard D's linked question) gives a good example: http://stackoverflow.com/questions/1269713/unit-tests-on-mvc-validation/3353125#3353125 – Dan Nov 17 '11 at 10:21
10

I have come across the same issue and while the accepted answer here did solve the "no-validation"-issue, it did leave me with a big negative aspect: it would throw an exception when there were validation errors instead of simply setting ModelState.Invalid to false.

I only tested this in Web Api 2 so I don't know what projects will have this available but there is a method ApiController.Validate(object) which forces validation on the passed object and only sets the ModelState.IsValid to false. Additionally you'll also have to instantiate the Configuration property.

Adding this code to my unit test allowed it to work:

userController.Configuration = new HttpConfiguration();
userController.Validate(addressInfo);
Community
  • 1
  • 1
Jeroen Vannevel
  • 43,651
  • 22
  • 107
  • 170
4

On another note. You should actually test what the controller returns and that the returned ActionResult is what you expect. Testing the ModelBinder should be done separately.

Let's say, you want to switch to a custom model binder. You could reuse the ModelBinder tests to the new ModelBinder you're creating. If your business rules remain the same, you should be able to directly reuse the same tests. However, if you mix your Controller test and ModelBinder tests and the test fails, you don't know if the problem is in the Controller or the ModelBinder.

Let's say you test your model binding something like this:

[Test]
public void Date_Can_Be_Pulled_Via_Provided_Month_Day_Year()
{
    // Arrange
    var formCollection = new NameValueCollection { 
        { "foo.month", "2" },
        { "foo.day", "12" },
        { "foo.year", "1964" }
    };

    var valueProvider = new NameValueCollectionValueProvider(formCollection, null);
    var modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(FwpUser));

    var bindingContext = new ModelBindingContext
    {
        ModelName = "foo",
        ValueProvider = valueProvider,
        ModelMetadata = modelMetadata
    };

    DateAndTimeModelBinder b = new DateAndTimeModelBinder { Month = "month", Day = "day", Year = "year" };
    ControllerContext controllerContext = new ControllerContext();

    // Act
    DateTime result = (DateTime)b.BindModel(controllerContext, bindingContext);

    // Assert
    Assert.AreEqual(DateTime.Parse("1964-02-12 12:00:00 am"), result);
}

Now that you KNOW, your model is bound right, you can continue testing the Model with your controller in a separate test to check if it returns you the correct result. Additionally, you can use the bound model values to test your Validation Attributes.

This way you can get a full set of tests which will reveal, if your application blows up, in which level it actually does that. ModelBinding, Controller or Validation.

Jani Hyytiäinen
  • 5,293
  • 36
  • 45
3
  1. Create Instance of your controller class.
  2. Add model state and call After adding model state
  3. the modelState always give false

    controller.ModelState.AddModelError("key", "error message");
    
    var invalidStateResult = _controller.Index();
    
    Assert.IsNotNull(invalidStateResult);
    
AlignedDev
  • 8,102
  • 9
  • 56
  • 91
ಅನಿಲ್
  • 606
  • 5
  • 7
2

Use controller.UpdateModel or controller.TryUpdateModel to use the controller's current ValueProvider to bind some data and trigger model binding validation prior to checking if the ModelState.IsValid

Jani Hyytiäinen
  • 5,293
  • 36
  • 45
0

If you want to test your validation action's behavior you could simply add ModelStateError:

ModelState.AddModelError("Password", "The Password field is required");
Felix
  • 830
  • 10
  • 17
  • But... If I'm changing the code in the test, than it doesn't test THE CODE, it tests A CODE... This is not what I was asking. but thanks. – gdoron Aug 28 '12 at 15:35
  • Otherwise you are testing MVC framework, and not your own code. If only you have your own validation check code, if so you should be testing it rather than controller's action... Quick question: what do you want to test: will it find the error? or What will happen if there is an error? – Felix Aug 28 '12 at 23:44
  • Will it find the error, because I have a plugin which should raise the error (`FluentValidation`) – gdoron Aug 29 '12 at 11:57
-2

Try controller.ViewModel.ModelState.IsValid instead of controller.ModelState.IsValid.

nogola
  • 214
  • 4
  • 12