Seeing anything you like? Google translate does not work out? Drop me a note and I will translate this post.
Dit is een vervolg op mijn eerdere blog: Custom client side validaties in ASP.NET MVC4
Als men vroeger (..) een MVC2 voorbeeld applicatie liet genereren dan kreeg men daar ook enkele custom validaties bij. Zo was er een validaties op het invoeren van tweemaal eenzelfde wachtwoord bij registratie. Dit voorbeeld werkte op class niveau om beide properties te vergelijken.
Een subtiele verbetering met de komst van MVC3 was dat dit opeens op property niveau ingesteld kon worden. Hierdoor was het mogelijk om de tekst van de validatie naast het tweede wachwoord veld te tonen.
Helaas wordt deze custom validatie niet meer gegenereerd maar is deze in het ASP.NET MVC framework als de CompareAttribute opgenomen. Gelukkig is ASP.NET MVC tegenwoordig open source en kan de broncode gedownload worden van http://aspnetwebstack.codeplex.com/SourceControl/changeset/view/3007f73f8cb2 (adres kan veranderen).
Door hier eens in te duiken heb ik een vergelijkbare validatie gebouwd. Deze controleert ook twee velden en wel of deze in de juiste volgorde gevuld zijn. Het tweede veld mag niet gevuld worden als de eerste nog niet gevuld is.
De client side code is als volgt:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class CompareSequenceAttribute : ValidationAttribute, IClientValidatable { private const string DefaultErrorMessage = "Fields {0} and {1} must be filled in the right sequence."; public CompareSequenceAttribute(string otherProperty) : base(DefaultErrorMessage) { if (otherProperty == null) { throw new ArgumentNullException("otherProperty"); } OtherProperty = otherProperty; } public string OtherProperty { get; private set; } public string OtherPropertyDisplayName { get; internal set; } public override string FormatErrorMessage(string name) { return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, OtherPropertyDisplayName ?? OtherProperty); } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { PropertyInfo otherPropertyInfo = validationContext.ObjectType.GetProperty(OtherProperty); if (otherPropertyInfo == null) { return new ValidationResult(String.Format(CultureInfo.CurrentCulture, "Unknown {0} to compare with", OtherProperty)); } object otherPropertyValue = otherPropertyInfo.GetValue( validationContext.ObjectInstance, null); if (!string.IsNullOrWhiteSpace(value as string) && (string.IsNullOrWhiteSpace(otherPropertyValue as string))) { if (OtherPropertyDisplayName == null) { OtherPropertyDisplayName = ModelMetadataProviders.Current. GetMetadataForProperty(() => validationContext.ObjectInstance, validationContext.ObjectType, OtherProperty).GetDisplayName(); } return new ValidationResult(FormatErrorMessage(validationContext.DisplayName)); } return null; } public IEnumerable<ModelClientValidationRule> GetClientValidationRules( ModelMetadata metadata, ControllerContext context) { if (metadata.ContainerType != null) { if (OtherPropertyDisplayName == null) { OtherPropertyDisplayName = ModelMetadataProviders.Current. GetMetadataForProperty(() => metadata.Model, metadata.ContainerType, OtherProperty).GetDisplayName(); } } var rule = new ModelClientValidationRule() { ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()), ValidationType = "comparesequence", }; rule.ValidationParameters.Add("other", OtherProperty); yield return rule; } }
De bijbehorende JavaScript zier er dan zo uit:
// validation name must be equal to the ValidationType in the ModelClientValidationRule $(function () { jQuery.validator.addMethod('compareSequence', function (value, element, param) { var other = param.other; var otherInput = $(element.form).find(":input[id='" + other + "']")[0]; if (value != null && value.length > 0 && (otherInput.value == null || otherInput.value == '')) { return false; } return true; }); jQuery.validator.unobtrusive.adapters.add( 'comparesequence', ['other'], function (options) { var params = { other: options.params.other }; options.rules['compareSequence'] = params; if (options.message) { options.messages['compareSequence'] = options.message; } }); } (jQuery));
Maar kunnen we deze code ook unittesten? Ja, en best eenvoudig!
De client side script unittesten is hier dan niet mee ondersteund maar de server side validatie test zal ik tonen. Overigens, heeft iemand ervaring met JavaScript unittest frameworks?
Nu is het zo dat een Validatie class een publieke IsValid methode heeft. Helaas is het simpel aanroepen van die IsValid() methode niet voldoende, Deze validatie heeft namelijk een ValidationContext nodig om bij de waarde van de andere property te komen en die context is lastig te injecteren.
En een viewmodel met een attribuut er op mocken is ook lastig. Dus ik heb maar voor stubs gekozen.De stub ziet er dan zo uit:
public class ViewModelToValidateCompareSequenceStub { public string FirstPropertyName { get; set; } [CompareSequence("FirstPropertyName")] public string SecondPropertyName { get; set; } }
En hiermee kan ik prima unittesten. Hier is een test waarbij de validatie goed gaat:
[TestMethod] public void CompareSequenceFirstFilledSecondFilledReturnsTrue() { //// ARRANGE var model = new ViewModelToValidateCompareSequenceStub { FirstPropertyName = "Bla", SecondPropertyName = "Bla" }; //// ACT var actual = Validator.TryValidateObject(model, new ValidationContext(model, null, null), null, true); //// ASSERT Assert.IsTrue(actual); }
En hier is een test waarbij het valideren fout gaat en dus een false teruggeeft:
[TestMethod] public void CompareSequenceFirstNullSecondFilledReturnsFalse() { //// ARRANGE var model = new ViewModelToValidateCompareSequenceStub { SecondPropertyName = "Bla" }; var results = new List<ValidationResult>(); //// ACT var actual = Validator.TryValidateObject(model, new ValidationContext(model, null, null), results, true); //// ASSERT Assert.IsFalse(actual); Assert.IsNotNull(results); Assert.AreEqual(1, results.Count); Assert.AreEqual( "Fields SecondPropertyName and FirstPropertyName must be filled in the right sequence.", results[0].ErrorMessage); }
Zoals te zien is, test ik ook op de specifieke foutmeldingen. Ik kan bv. ook een melding krijgen dat de opgegeven property niet bestaat. En er kunnen zelfs meerdere validaties gefaald hebben.
Conclusie
Het unittesten van validaties op viewmodellen is eenvoudig als men gebruik maakt van stubs. Enige nadeel is dat er geen controle is over de waarden die op de attributen ingesteld worden. Er moet dus voor andere varianten aparte stubs ingevuld worden.