Twitter Feed Popout byInfofru

OverrideThis.com

Adventures in .NET Software Craftsmanship!

MVC2 Validation and Testing – Refactored

This post is to fix a bug enhance the functionality in the original Blog Post titled “MVC2 Validation and Testing” at the following link. 
http://blog.overridethis.com/blog/post/2010/04/22/MVC2-Model-Validation-and-Testing-Scenarios.aspx


A while back I wrote about testing Controller Actions that rely on MVC2’s new Model Validation.  I had been using the technique described in the post for a while on most of my MVC2 applications without running into any significant issue until I tried to combine validators on a single property of my Model.  The following are the modified testing scenarios that would not work with the code I provided in my originally post.  Apparently, the code I wrote originally does not target all validators that are applied through Metadata.

 

namespace WebSite.Services {

    public interface ICommentService {
        void AddComment(string email, string message);
    }

}
namespace WebSite.Models {
    
    using System.ComponentModel.DataAnnotations;

    public class AddCommentRequest {
        private const string EMAIL_REGEX = 
           @"^(([\w-]+\.)+[\w-]+|([a-zA-Z]{1}|[\w-]{2,}))@"
            + @"((([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\.([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\."
            + @"([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\.([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])){1}|"
            + @"([a-zA-Z]+[\w-]+\.)+[a-zA-Z]{2,4})$";
        [Required, RegularExpression(EMAIL_REGEX)]
        public string Email { get; set; }
        [Required]
        public string Message { get; set; }
    }

    public class AddCommentResult {
        public bool Success { get; set; }
        public string Message { get; set; }
    }
}
namespace WebSite.Controllers {

    using System.Web.Mvc;
    using Models;
    using Services;

    public class CommentController : Controller {

        private readonly ICommentService service;

        public CommentController() {

        }

        public CommentController(ICommentService service) {
            this.service = service;
        }

        [HttpPost]
        public ActionResult AddComment(AddCommentRequest request) {
            if (ModelState.IsValid) {
                this.service.AddComment(request.Email, request.Message);
                return Json(new AddCommentResult {Success = true, Message = "Success"});
            }
            return Json(
                new AddCommentResult { 
                        Success = false, 
                        Message = "The 'message' is a required field." 
                });
        }
    }
}

 

The following Test validate the Model Validation functionality enforce and needless to say if we use the original ControllerExtension for validation we get less than ideal results.

 

[TestFixture]
public class CommentControllerTests {

    private string VALID_MESSAGE = "SOME MESSAGE!";
    private string VALID_EMAIL   = "someemail@yahoo.com";
    private string NOT_VALID_MESSAGE = string.Empty;
    private string NOT_VALID_EMAIL   = "someemail@@@@@@c";
    
    [Test]
    public void CommentControllerCanAddValidMessage() {
        Assert.IsTrue(this.HttpPostToAddComments(VALID_EMAIL, VALID_MESSAGE).Success);
    }

    [Test]
    public void CommentControllerCannotAddWithoutValidMessage() {
        Assert.IsTrue(this.HttpPostToAddComments(VALID_EMAIL, NOT_VALID_MESSAGE).Success);
    }    

    [Test]
    public void CommentControllerCannotAddWithoutRequiredEmail() {
        Assert.IsTrue(this.HttpPostToAddComments("", VALID_MESSAGE).Success);
    }    

    [Test]
    public void CommentControllerCannotAddWithourValidEmailAddress() {
        Assert.IsTrue(this.HttpPostToAddComments(NOT_VALID_EMAIL, VALID_MESSAGE).Success);
    }

    private AddCommentResult HttpPostToAddComments(string email, string message) {
        
        var mockOfICommentService = new Mock<ICommentService>();
        mockOfICommentService.Setup(m => m.AddComment(VALID_EMAIL, VALID_MESSAGE)).AtMostOnce();
        var model = new AddCommentRequest {Email = email, Message = message};
        var controller = new CommentController(mockOfICommentService.Object);
        var result = controller
		.CallWithModelValidation(c => c.AddComment(model), model);
        mockOfICommentService.Verify();
        return (AddCommentResult)((JsonResult) result).Data;
    }
}

 

This time around I feel like I did better research to find a suitable solution by looking at the ASP.NET MVC 2 source code and its tests.   In my original post, I had overlooked a couple of reusable tools already in place in the ASP.NET MVC source code that would have made my work a whole lot easier like the ModelMetadataProviders, and DataAnnotationsModelValidatorProvider.   Of course, there is always room for improvement, but the following is the refactored ControllerExtensions class that allows for all the previous tests to go green.

 

public static class ControllerExtensions {

    public static ActionResult CallWithModelValidation<C, R, T>(this C controller
           , Func<C, R> action
           , T model)
        where C : Controller
        where R : ActionResult
        where T : class {
        DataAnnotationsModelValidatorProvider provider = new DataAnnotationsModelValidatorProvider();
        IEnumerable<ModelMetadata> metadata = ModelMetadataProviders
			.Current
			.GetMetadataForProperties(model, typeof(T));
        foreach (ModelMetadata modelMetadata in metadata) {
            IEnumerable<ModelValidator> validators = provider
				.GetValidators(modelMetadata, new ControllerContext());
            foreach (ModelValidator validator in validators) {
                IEnumerable<ModelValidationResult> results = validator.Validate(model);
                foreach (ModelValidationResult result in results)
                    controller.ModelState.AddModelError(modelMetadata.PropertyName, result.Message);
            }
        }
        return action(controller);
    }
}