Yesterday I wrote about ASP.NET MVC, TDD and AutoMapper, and how you can use them together in a DDD application. Today I thought I would follow up and explain how to apply these techniques to another important (but boring) part of any web application: user input validation.
To achieve this, we are using Fluent Validation, a validation framework that lets you easily set up validation rules using a fluent syntax:
public class UserRegistrationFormValidator : AbstractValidator<UserRegistrationForm> { public UserRegistrationFormValidator() { RuleFor(f => f.Username).NotEmpty() .WithMessage( "You must choose a username!" ); RuleFor(f => f.Email).EmailAddress() .When(f => !String.IsNullOrEmpty(f.Email)) .WithMessage( "This doesn't look like a valid e-mail address!" ); RuleFor(f => f.Url).MustSatisfy( new ValidWebsiteUrlSpecification()) .When(f => !String.IsNullOrEmpty(f.Url)) .WithMessage( "This doesn't look like a valid URL!" ); } } |
If you think about it, validation and view model mapping have similar footprints in the application. They both:
- Live in the application services layer
- May invoke domain services
- Use third-party libraries
- Have standalone fluent configurations
- Have standalone tests
- Are injected into the application services
Let’s see how it all fits together starting at the outermost layer, the controller.
public class AccountController : Controller { readonly IUserRegistrationService registrationService; readonly IFormsAuthentication formsAuth; ... [AcceptVerbs(HttpVerbs.Post)] public ActionResult Register(UserRegistrationForm user) { if (user == null ) throw new ArgumentNullException( "user" ); try { this .registrationService.RegisterNewUser(user); this .formsAuth.SignIn(user.Username, false ); return RedirectToAction( "Index" , "Home" ); } catch (ValidationException e) { e.Result.AddToModelState( this .ModelState, "user" ); return View( "Register" , user); } } ... } |
As usual, the controller is pretty thin, delegating all responsibility (including performing any required validation) to an application service that handles new user registration. If validation fails, all our controller has to do is catch an exception and append the validation messages contained within to the model state to tell the user any mistakes they made.
The UserRegistrationForm validator is injected into the application service along with any others. Just like AutoMapper, we can now test both the controller, validator and application service separately.
public class UserRegistrationService : IUserRegistrationService { readonly IUserRepository users; readonly IValidator<UserRegistrationForm> validator; ... public void RegisterNewUser(UserRegistrationForm form) { if (form == null ) throw new ArgumentNullException( "form" ); this .validator.ValidateAndThrow(form); User user = new UserBuilder() .WithUsername(form.Username) .WithAbout(form.About) .WithEmail(form.Email) .WithLocation(form.Location) .WithOpenId(form.OpenId) .WithUrl(form.Url); this .users.Save(user); } } |
Testing the user registration form validation rules
Fluent Validation has some nifty helper extensions that make unit testing a breeze:
[TestFixture] public class When_validating_a_new_user_form { IValidator<UserRegistrationForm> validator = new UserRegistrationFormValidator(); [Test] public void The_username_cannot_be_empty() { validator.ShouldHaveValidationErrorFor(f => f.Username, "" ); } [Test] public void A_valid_email_address_must_be_provided() { validator.ShouldHaveValidationErrorFor(f => f.Email, "" ); } [Test] public void The_url_must_be_valid() { validator.ShouldNotHaveValidationErrorFor(f => f.Url, "http://foo.bar" ); } } |
You can even inject dependencies into the validator and mock them out for testing. For example, in this app the validator calls an IUsernameAvailabilityService to make sure the chosen username is still available.
Testing the user registration service
This validation code is now completely isolated, and we can mock out the entire thing when testing the application service:
[TestFixture] public class When_registering_a_new_user { IUserRegistrationService registrationService; Mock<IUserRepository> repository; Mock<IValidator<UserRegistrationForm>> validator; [Test, ExpectedException( typeof (ValidationException))] public void Should_throw_a_validation_exception_if_the_form_is_invalid() { validator.Setup(v => v.Validate(It.IsAny<UserRegistrationForm>())) .Returns(ObjectMother.GetFailingValidationResult()); service.RegisterNewUser(ObjectMother.GetNewUserForm()); } [Test] public void Should_add_the_new_user_to_the_repository() { var form = ObjectMother.GetNewUserForm(); registrationService.RegisterNewUser(form); service.Verify( r => r.Save(It.Is<User>(u => u.Username.Equals(form.Username)))); } } |
Testing the accounts controller
With validation out of the way, all we have to test on the controller is whether or not it appends the validation errors to the model state. Here are the fixtures for the success/failure scenarios:
[TestFixture] public class When_successfully_registering_a_new_user : AccountControllerTestContext { [SetUp] public override void SetUp() { ... result = controller.Register(form); } [Test] public void Should_register_the_new_user() { registrationService.Verify(s => s.RegisterNewUser(form), Times.Exactly(1)); } [Test] public void Should_sign_in() { formsAuth.Verify(a => a.SignIn(user.Username, false )); } } [TestFixture] public class When_registering_an_invalid_user : AccountControllerTestContext { [SetUp] public override void SetUp() { ... registrationService.Setup(s => s.RegisterNewUser(form)).Throws( new ValidationException( ObjectMother.GetFailingValidationResult())); result = controller.Register(form); } [Test] public void Should_not_sign_in() { formsAuth.Verify(a => a.SignIn(It.IsAny< string >(), It.IsAny< bool >()), Times.Never()); } [Test] public void Should_redirect_back_to_the_register_view_with_the_form_contents() { result.AssertViewRendered().ForView( "Register" ) .WithViewData<UserRegistrationForm>().ShouldEqual(form); } } |
This post has been a bit heavier on code than usual, but hopefully it is enough to get an idea of how easy it is to implement Fluent Validation in your ASP.NET MVC application.