In a previous post, I mentioned that model validation should be tested separately from controller logic. I will demonstrate a way of unit testing the validation of models implemented with System.ComponentModel.DataAnnotations.
It is actually quire easy to unit test model validation. Models are inherently easy to test separately due to their POD (Plain Old Data) nature. We can instantiate them directly. Moreover, DataAnnotations provides us with the necessary interface to run the validation against a model object completely separately from the rest of the application.
A first model validation test class
Here is a basic model that we will unit test for the demonstration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using System; using System.ComponentModel.DataAnnotations; namespace DataAnnotationsUnitTesting { public class CreatePersonModel { [Required] public string FirstName { get; set; } [Required] public string LastName { get; set; } [Range(typeof (DateTime), "1900/01/01", "2000/01/01")] public DateTime BirthDate { get; set; } [RegularExpression(@"\+?\d+")] public string PhoneNumber { get; set; } } } |
As you can see, it is quite simple. We’ll go directly to the unit test implementation.
The strategy we are going to use consists in basing each test case on a valid model instance, then modifying it in such a way that it triggers one single validation error. We end up with a skeleton unit test class like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using NUnit.Framework; namespace DataAnnotationsUnitTesting { [TestFixture] public class CreatePersonModelValidationTests : AssertionHelper { private CreatePersonModel CreateValidPersonModel() { return new CreatePersonModel() { FirstName = "Some Name", LastName = "Some Last Name", BirthDate = new DateTime(1982, 07, 28), PhoneNumber = "+112233445566" }; } public IEnumerable<ValidateRuleSpec> ValidationRule_Source() { yield break; } [Test] [TestCaseSource("ValidationRule_Source")] public void ValidationRule(ValidateRuleSpec spec) { // Arrange var model = CreateValidPersonModel(); // Apply bad valud model.GetType().GetProperty(spec.MemberName).SetValue(model, spec.BadValue); // Act var validationResults = new List<ValidationResult>(); var success = Validator.TryValidateObject(model, new ValidationContext(model), validationResults, true); // Assert Expect(success, False); Expect(validationResults.Count, EqualTo(1)); Expect(validationResults.SingleOrDefault(r => r.MemberNames.Contains(spec.MemberName)), Not.Null); } public class ValidateRuleSpec { public object BadValue; public string MemberName; public override string ToString() { return MemberName + " - " + (BadValue ?? "<null>"); } } } } |
Some explanation:
- ValidateRule() implements the test itself. However, it gets the specifications for each test via an argument.
- ValidateRule_Source() provides the specs for our test.
- class ValidateRuleSpec holds the specifications. Its ToString() uses the spec values to render a distinct string per test. This makes unit test reports easy to read. In case of failure, you know exactly which spec failed.
And now the implementation of our ValidateRule_Source():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
public IEnumerable<ValidateRuleSpec> ValidationRule_Source() { yield return new ValidateRuleSpec() { BadValue = null, MemberName = "FirstName" }; yield return new ValidateRuleSpec() { BadValue = string.Empty, MemberName = "FirstName" }; yield return new ValidateRuleSpec() { BadValue = null, MemberName = "LastName" }; yield return new ValidateRuleSpec() { BadValue = string.Empty, MemberName = "LastName" }; yield return new ValidateRuleSpec() { BadValue = new DateTime(), MemberName = "BirthDate" }; yield return new ValidateRuleSpec() { BadValue = DateTime.Today, MemberName = "BirthDate" }; yield return new ValidateRuleSpec() { BadValue = "-65623", MemberName = "PhoneNumber" }; yield return new ValidateRuleSpec() { BadValue = "abc", MemberName = "PhoneNumber" }; } |
Refining the solution
This works but can be improved. Most of the functionality can be abstracted. The actual test need only provide the valid model object and the specifications. A bit of refactoring yields a nicer design for our test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using NUnit.Framework; namespace DataAnnotationsUnitTesting { [TestFixture] public abstract class ModelValidationTestsBase : AssertionHelper { protected abstract CreatePersonModel CreateValidModel(); public abstract IEnumerable<ValidateRuleSpec> ValidationRule_Source(); [Test] [TestCaseSource("ValidationRule_Source")] public void ValidationRule(ValidateRuleSpec spec) { // Arrange var model = CreateValidModel(); // Apply bad valud model.GetType().GetProperty(spec.MemberName).SetValue(model, spec.BadValue); // Act var validationResults = new List<ValidationResult>(); var success = Validator.TryValidateObject(model, new ValidationContext(model), validationResults, true); // Assert Expect(success, False); Expect(validationResults.Count, EqualTo(1)); Expect(validationResults.SingleOrDefault(r => r.MemberNames.Contains(spec.MemberName)), Not.Null); } protected ValidateRuleSpec Spec(string memberName, object badValue) { return new ValidateRuleSpec() {MemberName = memberName, BadValue = badValue}; } public class ValidateRuleSpec { public object BadValue; public string MemberName; public override string ToString() { return MemberName + " - " + (BadValue ?? "<null>"); } } } public class CreatePersonModelValidationTests : ModelValidationTestsBase { protected override CreatePersonModel CreateValidModel() { return new CreatePersonModel() { FirstName = "Some Name", LastName = "Some Last Name", BirthDate = new DateTime(1982, 07, 28), PhoneNumber = "+112233445566" }; } public override IEnumerable<ValidateRuleSpec> ValidationRule_Source() { yield return Spec("FirstName", null); yield return Spec("FirstName", string.Empty); yield return Spec("LastName", null); yield return Spec("LastName", string.Empty); yield return Spec("BirthDate", new DateTime()); yield return Spec("BirthDate", DateTime.Today); yield return Spec("PhoneNumber", "-65623"); yield return Spec("PhoneNumber", "abc"); } } } |
Notice how we trimmed CreatePersonModelValidationTests to a minimum. The class ModelValidationTestsBase can now be used for most of our model validation unit tests.
What do you think?