One of the biggest advantages ASP.NET MVC has over ASP.NET Web Forms is the ability to unit-test your views (UI), controllers (HTTP request handlers) and view models (data) easily thanks to the separation of concerns that the MVC pattern gives you. Unit-testing your controllers is pretty straight forward because they either return views or perform redirections, and testing them can be done with the help of a mocking framework such as Moq to mock out the controller’s dependencies. However testing your view models is a little bit more tricky because the model-binding and validation mechanisms which MVC provides at runtime when handling HTTP requests, don’t automatically kick-in when a test calls a controller action method directly. This means that your unit-tests have to provide those mechanisms themselves.
This sounds slightly daunting but once you have some reusable code in a base or helper class, it’s actually quite simple. The following example was written in ASP.NET MVC 2 but should work quite happily in MVC 3. And just to be clear, whenever I mention model here, I’m referring to an MVC view model (which is nothing more than a data transfer object) and not a domain model which contains business logic.
Testing View Model Validation
One of the first obstacles I hit with MVC was trying to test that view model validation was working for a large HTML form which was being posted by one of my views. Because model-binding wasn’t happening automatically, validation wasn’t being triggered and my tests all failed when they should have been passing. The solution is two-fold: you need a base or helper method which actually performs model validation, and you then need to call that method before calling your controller action.
One of the first obstacles I hit with MVC was trying to test that view model validation was working for a large HTML form which was being posted by one of my views. Because model-binding wasn’t happening automatically, validation wasn’t being triggered and my tests all failed when they should have been passing. The solution is two-fold: you need a base or helper method which actually performs model validation, and you then need to call that method before calling your controller action.
Under normal TDD rules, you should always write your tests first. But for this example I’m showing you the test last because that’s where the important part happens and it helps to be familiar with what the other classes are doing first. So to start off with, here’s a simple customer view model with a few properties, and some regular expression validation on the Email property to check that the email address is a valid format. To make these validation attributes available, you will need to add a .NET reference to
System.ComponentModel.DataAnnotations
and you will also need to add a using statement for the System.ComponentModel.DataAnnotations
namespace in your view model class file. The regular expression just checks that the email address is in the correct format and is not specific to this topic – it could be any regular expression on any property.
And here is the controller which receives the
Customer
view model as a parameter in its overloaded CreateAccount
action method which handles form posts. There is some standard logic to check whether the view model (form data) that was posted is valid and if not, the same view is sent back in the response together with the values that were submitted. This is so that previously entered form data isn’t lost – an important aspect of the post/validate process. However if the view model is valid, then it saves the customer details (not shown) and redirects the user to the Login
action (again, not shown). This is important for our test because a successful redirection tells us that the view model is valid, so that is what our test will assert on.
To test that the model is valid, here is a reusable method to validate view models – a process normally done by the MVC runtime. This can be placed in a base class for your MVC tests, as a static helper method or as an extension method on the
System.Web.Mvc.Controller
class. I’ll leave that choice down to you but for this example I’ve made it an extension of the MVC Controller base class.
The first line of the extension method is crucial because it clears previous model validation results. Otherwise your tests could potentially end up with false negatives or even worse, false positives because it still contains previous view model validation results. The rest of the extension method is doing what the MVC framework normally does automatically. It validates the view model and generates an invalid results collection which is then added as errors to the controller’s model state.
Now all you need to do is call the helper from within your tests after creating your controller and view model but before calling the action method which takes the model as a parameter. Here is an example NUnit test fixture showing how it’s used. I’ve used the Arrange-Act-Assert (AAA) style for clarity and highlighted one of the calls to the extension method. Remember, it must be called before calling the controller action.
Note! if you get a missing assembly reference error after trying to access properties on the RedirectToRouteResult class (as I have done in the assertions above) make sure your test project references both the
System.Web.dll
and System.Web.Routing.dll
assemblies.
As you can see, the expected behaviour for the
CreateAccount
action is that when given a view model containing an invalid email address, it returns the same view and the result’s ViewData.ModelState.IsValid property is set to false. But when given a view model with a valid email address, it passes validation and causes a redirection – in this scenario you don’t need to assert the value of ViewData.ModelState.IsValid because the fact that a redirection occured (or whatever successful outcome you were expecting) tells you that validation was successful.
And just incase you’re confused why there’s an assertion in the first test to check that the view result’s ViewName property is an empty string, that’s because it’s the expected value for ViewName when an action returns the default view.
This works fine for built-in and custom validation attributes, but will not execute the Remote validator. Is there any way to get Remote validator errors?
ReplyDeleteThanks