Unittests voor MVC4 model validaties

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.

valunit1

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.

Advertentie

Custom client side validaties in ASP.NET MVC4

Seeing anything you like? Google translate does not work out? Drop me a note and I will translate this post.

Kijk voor een vervolg op deze blog naar: Unittests voor MVC4 model validaties

Eén van de meest onderschatte aspecten van gebruikersvriendelijke websites is het toepassen van validaties. Validaties kunnen teruggevonden worden op drie locaties:

  1. Validaties diep de businesslaag (of zelfs in de datalaag)
  2. Validaties op het ASP.NET MVC (view-)model welke op de server gevalideerd worden door de controller
  3. Validaties welke op de client uitgevoerd worden via JavaScript.

De validaties uit 1 zijn lastig om te valideren zonder een daadwerkelijke aanroep van de onderliggende lagen van de website.

Voor de twee andere varianten geldt dat niet. De validaties uit 2 zijn geschreven in .Net code, de validaties uit 3 zijn geschreven in JavaScript. ASP.NET MVC maakt het mogelijk om eenzelfde soort validatie aan elkaar te laten refereren. Hierdoor kan via de Razor view engine de HTML/Javascript voor de client side validatie aangemaakt worden als een eigenschap op het (view-)model een server side validatie implementeert…

Hier volgt een stappenplan voor het schrijven van een Custom Client Side Validatie.

Stap 1

Eigenlijk overbodig, deze opties staan tegenwoordig standaard aan,  maar in de web.config moet het toepassen van client validaties aan staan. Controleer dus of de volgende twee opties op true staan:


<appSettings>

…

<add key="ClientValidationEnabled" value="true" />

<add key="UnobtrusiveJavaScriptEnabled" value="true" />

</appSettings>


Stap 2

Laten we eerst de server side validatie aanmaken. Ik heb als voorbeeld bij gebrek aan inspiratie een validatie geschreven die de minimale lengte van een string property controleert. De server side validatie is volgt:

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class ValidateMinimalLengthAttribute : ValidationAttribute
{
    private const string DefaultErrorMessage = "{0} must be at least {1} characters long.";

    public ValidateMinimalLengthAttribute()
        : base(DefaultErrorMessage)
    {
    }

    public int MinimalLength { get; set; }

    public override string FormatErrorMessage(string name)
    {
        return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString, name, MinimalLength);
    }

    public override bool IsValid(object value)
    {
        var valueAsString = value as string;

        return (string.IsNullOrEmpty(valueAsSString)) || valueAsString.Length >= MinimalLength;
    }
}

Stap 3

Deze validatie is direct toe te passen op ons model:

public class CustomerModel
{
    public int Id { get; set; }

    public string Name { get; set; }

    [Display(Name = "City")]
    [ValidateMinimalLength(MinimalLength = 10)]
    public string Address1City { get; set; }
}

Stap 4

En deze validatie kan op de controller gecontroleerd worden:

public class CustomerController : Controller
{
    public ActionResult Create()
    {
        return View(new CustomerModel());
    }

    [HttpPost]
    public ActionResult Create(CustomerModel model)
    {
        if (ModelState.IsValid)
        {
            // model is valid
        }
        else
        {
            // model is invalid
        }

        return View();
    }
}

Het toevoegen van server side validaties is dus tamelijk eenvoudig. Laten we nu aan de client side validatie gaan werken…

Stap 5

Om te beginnen moeten we aan de bestaande validatie kennis meegeven dat er een client side validatie beschikbaar komt. We laten de ValidateMinimalLengthAttribute de interface IClientValidatable implementeren:


[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class ValidateMinimalLengthAttribute : ValidationAttribute, IClientValidatable
{
  …

  public override string FormatErrorMessage(string name)
  {
    …
  }

  public override bool IsValid(object value)
  {
    …
  }

  public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
    ModelMetadata metadata,  ControllerContext context)
  {
    //// from metadata: ShortDisplayName > DisplayName > PropertyName
    var messageNameDisplayName = (!string.IsNullOrWhiteSpace(metadata.ShortDisplayName))
               ? metadata.ShortDisplayName
               : metadata.DisplayName;

    var messageName = (!string.IsNullOrWhiteSpace(messageNameDisplayName))
               ? messageNameDisplayName
               : metadata.PropertyName;

    var rule = new ModelClientValidationRule()
    {
      ErrorMessage = FormatErrorMessage(messageName),
      ValidationType = "minimallength",
    };

    rule.ValidationParameters.Add("minlength", MinimalLength);

    yield return rule;
  }
}

Deze implementeert de methode GetClientValidationRules welke een opsomming van uit te voeren validaties opgeeft. In ons geval geven we aan dat er op de client een validatie genaamd “minimallength” gerefereerd moet worden en we laten de eventueel te tonen errormessage doorgeven. Ook laten we MinimalLength integer doorgeven. Deze informatie zal door de Razor viewengine gebruikt worden om de HTML aan te maken voor deze unobtrusive validatie. We definiëren in de view:

<div>
  @Html.LabelFor(model => model.Address1City)
</div>
<div>
  @Html.EditorFor(model => model.Address1City)
  @Html.ValidationMessageFor(model => model.Address1City)
</div>

De uiteindelijk gegenereerd HTML ziet er dan zo uit:

<div>
  <label for="Address1City">City</label>
</div>
<div>
  <input data-val="true"
         data-val-minimallength="City must be at least 10 characters long."
         data-val-minimallength-minlength="10"
         id="Address1City"
         name="Address1City"
         type="text"
         value="" />
  <span data-valmsg-for="Address1City"
        data-valmsg-replace="true"></span>
</div>

Let op! Wat we hier zien zijn slechts enkele extra HTML attributen op een input veld. Dit is nog niet voldoende voor de validatie want we hebben nog niet eens de javascript functie geschreven!

Laten we dus snel verder gaan.

Stap 6

We moeten dus een functie schrijven die de lengte valideert en deze functie moet geregistreerd worden onder de naam ‘minimallength’. De volgende javascript voldoet hieraan:

// validation name must be equal to the ValidationType in the ModelClientValidationRule

$(function () {
    jQuery.validator.addMethod('minimalLength', function (value, element, param) {
        var minlength = param.minlength;

        if (value != null && value.length > 0 && value.length < minlength) {
            return false;
        }
        return true;
    });

    jQuery.validator.unobtrusive.adapters.add('minimallength', ['minlength'], function (options) {
        var params = {
            minlength: options.params.minlength
        };

        options.rules['minimalLength'] = params;

        if (options.message) {
            options.messages['minimalLength'] = options.message;
        }
    });
} (jQuery));

Wat we hier zien is de functie (met de hoofdletter in de naam) en de registratie ervan. Interessant is om te zien hoe de parameter ‘minlength’ doorgegeven wordt tijdens de registratie, tesamen met de message.

Stap 7

Maar deze code moet wel op de client beschikbaar gesteld zijn, en wel na het laden van bibliotheken van de jQuery validaties en de unobtrusive code.

Hiervoor kan handig meegelift worden met de ‘~/bundles/jqueryval’ bundle. In de bundle configuratie vullen we de validatie bundle aan met ons eigen script (deze heb ik in een aparte Validations map gestopt):

bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
            "~/Scripts/jquery.unobtrusive*",
            "~/Scripts/jquery.validate*",
            "~/Validations/MinimalLength.js"
            ));

En, heel prettig, laat deze bundle nu net al standaard gerefereerd en dus geladen worden door views welke met de standaard Create en Edit templates aangemaakt zijn:

@model MvcApplication1.Models.CustomerModel
@{
    ViewBag.Title = "Create";
}
…
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Stap 8

Zo, nu kan alles getest worden. Start de website en ga naar de pagina waarin de validatie actief moet zijn. Controleer eerst even of de data-val attributen geplaatst zijn. Probeer vervolgens eens de client side validatie uit:

clientsideval1

Bonus

Ok, het werkt prima, maar hoe valideer je nu of de server side variant ook nog werkt? Schakel hiervoor in de web.config de ClientValidationEnabled uit. Hierdoor blijft de attributen voor unobtrusive script dus achterwege:

<div class=”editor-label”>
  <label for="Address1City">City</label></div>
<div>
<input id="Address1City"
       name="Address1City"
       type="text"
       value=""   />
<span>City must be at least 10 characters long.</span>
</div>

Conclusie

Client side validatie geeft een hele goede web-ervaring voor gebruikers van jouw site. Met de komst van MVC 3 is het toepassen van unobtrusive validaties een eenvoudige en mooie aanvulling van toe te passen technieken geworden voor de moderne webontwikkelaar. En deze validaties zijn keer op keer her te gebruiken over meerdere projecten.