Thomas Bandt

Über mich | Kontakt | Archiv

ASP.NET MVC 2 - Unit Tests für Data Annotations

Die neue Vorgehensweise beim Validieren von Daten über View Models und Attribute erfordert auch eine kleine Umstellung bei der test-getriebenen Entwicklung (TDD).

So kann man zwar weiterhin die Controller-Action auf die Anforderungen hin testen, etwa ob der ModelState ungültig ist, wenn eine per Attribut deklarierte Bedingung an ein Element des View Models nicht erfüllt ist (z.B. "required"), aber eigentlich ist das der falsche Ort - viel mehr sollte man das View Model selbst testen.

Um das tun zu können, muss man während des Testens an die Attribute herankommen. Das geht natürlich per Reflection, aber Microsoft hat mitgedacht und im Namespace System.ComponentModel.DataAnnotations eine Helfer-Klasse namens Validator mitgeliefert (.NET 4.0).

Mit der darin befindlichen Methode TryValidateObject lässt sich das SUT (System under Test) überprüfen.

   1:  public class SignUpViewModel
   2:  {
   3:   
   4:      [Required(ErrorMessage = "Bitte geben Sie Ihren Nachnamen an")]
   5:      public string UserLastName { get; set; }
   6:   
   7:      [BooleanRequiredToBeTrue(ErrorMessage = "Bitte lesen und akzeptieren Sie die AGB.")]
   8:      public bool AgreesWithTerms { get; set; }
   9:   
  10:  }

   1:  [TestMethod]
   2:  public void Der_Nachname_darf_kein_Leerstring_sein()
   3:  {
   4:      var sut = new SignUpViewModel { UserLastName = " " };
   5:      var validationResults = new List<ValidationResult>();
   6:      var result = Validator.TryValidateObject(sut, 
   7:          new ValidationContext(sut, null, null), validationResults);
   8:      Assert.IsFalse(result);
   9:  }

Allerdings ist das noch nicht optimal. Das SUT ist zwar in diesem Fall nicht valide, aber ob das daran liegt, dass "AgreeWithTerms" false ist oder ob der Nachname ein Leerstring ist, ist unklar.

Die von der Prüfmethode TryValidateObject zurückgelieferte Liste an Fehlern  enthält aber zum Glück auch die dazugehörigen Informationen, die nötig sind um zu prüfen, ob nun genau "UserLastName" invalide ist.

Hierzu habe ich mir eine kleine Erweiterung geschrieben, die die Prüfung übernimmt:

   1:  public class ValidationResults : List<ValidationResult>
   2:  {
   3:  }
   4:   
   5:  public static class TestHelper
   6:  {
   7:   
   8:      public static bool ContainsField<T>(this ValidationResults results, 
   9:          Expression<Func<T, object>> action)
  10:      {
  11:          string name = null;
  12:   
  13:          if (action.Body is MemberExpression)
  14:              name = ((MemberExpression) action.Body).Member.Name;
  15:   
  16:          if (action.Body is UnaryExpression)
  17:              name = ((UnaryExpression) action.Body).Operand.ToString().Split('.')[1];
  18:   
  19:          if(name == null)
  20:              throw new ArgumentException("Unbekannter Expression-Typ, bitte ergänzen");
  21:   
  22:          return results.Any(names => names.MemberNames.Contains(name));
  23:      }
  24:   
  25:  }

Die etwas aufwendige und nach Handarbeit aussehende Vorgehensweise scheint nötig, da "action.Body" unterschiedlichen Typs sein kann (üblicherweise MemberExpression, bei Bools aber bsp. UnaryExpression) und so kein einheitlicher Zugriff auf den gesuchten "Namen" möglich ist (falls jemand eine Alternative weiß ... :-)). Der Aufruf erfolgt wie folgt:

   1:  [TestMethod]
   2:  public void Der_Nachname_darf_kein_Leerstring_sein()
   3:  {
   4:      var sut = new SignUpViewModel { UserLastName = " " };
   5:      var validationResults = new ValidationResults();
   6:      Validator.TryValidateObject(sut, new ValidationContext(sut, null, null), 
   7:          validationResults, true);
   8:      Assert.IsTrue(validationResults.
   9:          ContainsField<SignUpViewModel>(m => m.UserLastName));
  10:  }

Nett daran ist vor allem die gegebene Typsicherheit, so dass auf "Magic Strings" verzichtet werden kann und auch im Fall einer Refaktorisierung keine Probleme entstehen und Tests von Hand nachgezogen werden müssen.

Kommentare

  1. Mathias schrieb am Dienstag, 16. Februar 2010 16:56:00 Uhr:

    Ist das noch ein Unit Test oder ist das nicht schon eher ein Integration Test? Testest Du hier nicht eher die TryValidateObject-Methode und weniger das ViewModel?

    Gab schon einige gute Blogposts zum Thema, z.B. http://blog.stevensanderson.com/2009/08/24/writing-great-unit-tests-best-and-worst-practises ("Don't unit test configuration settings")
  2. Thomas schrieb am Dienstag, 16. Februar 2010 17:01:00 Uhr:

    Konfiguration ist das in meinen Augen nicht, letztendlich verschiebt sich die Verantwortung um die Validierung in dem Fall nur von der Controller-Methode hin zum View-Model.

    Wie man das Kind nennt ist eine andere Sache, ich bin da durchaus auch einverstanden, das Integrationstest zu nennen. Aber letztlich wäre das ein Streit um des Kaisers Bart.
  3. Thomas goes .NET schrieb am Dienstag, 16. Februar 2010 17:09:00 Uhr:

    Ein Standard-Fall im Web ist die Eingabe von Passwörtern für Registrierungs-Formulare. Damit man auch (weitestgehend) sicherstellt, dass das eingegebene Passwort keine unbeabsichtigten Tippfehler enthält, lässt man es den Nutzer üblicherweise einfach ...
  4. Mathias schrieb am Dienstag, 16. Februar 2010 17:18:00 Uhr:

    Ja. Ich bin da auch noch unentschlossen. ob man das nun Unit Test nennt oder nicht. Hauptsache man testet es überhaupt irgendwo.
  5. Thomas goes .NET schrieb am Samstag, 15. Mai 2010 11:05:00 Uhr:

    Ich habe mich zwar schon vor einiger Zeit von der Verwendung der DataAnnotations in meinen ernsthaften Projekten wieder verabschiedet, doch habe ich just diese Woche erst wieder ein kleineres Projekt damit umgesetzt (man glaubt ja gar nicht wie effiz ...


« Zurück  |  Weiter »