Thomas Bandt

Über mich | Kontakt | Archiv

Repositories testen?

Wie ich aktuell test-getrieben entwickle habe ich kürzlich in diesem Post beschrieben. Was ich darin verschwiegen habe ist, wie ich meinen Datenzugriff teste.

Den Zugriff auf die Daten einer Anwendung kapsele ich in Repositories. Dies verhindert Abhängigkeiten zu und von anderen Teilen der Anwendung und sorgt für eine 100% einwandfreie Trennung, da alle anderen Komponenten nur gegen ein Interface arbeiten:

   1:  public interface ICompanyRepository
   2:  {
   3:      void CreateCompany(Company company);
   4:      Company GetCompanyByName(Guid clientID, string name);
   5:  }

Für die konkrete Implementierung bevorzuge ich seit Längerem das von vielen belächelte Linq to SQL. Da ich aktuell fast immer einen SQL Server als Datenbank verwende deckt das meine Bedürfnisse ganz gut ab.

Beispiel für eine Implementierung:

   1:  public class CompanyRepository : RepositoryBase, ICompanyRepository
   2:  {
   3:   
   4:      public CompanyRepository(IConfigurationService config) : base(config)
   5:      {
   6:      }
   7:   
   8:      public void CreateCompany(Model.Company company)
   9:      {
  10:          var newCompany = new Company();
  11:          newCompany.ID = Guid.NewGuid();
  12:          newCompany.Created = DateTime.Now;
  13:          newCompany.FKClientID = company.ClientID;
  14:          newCompany.Language = (int)company.Language;
  15:          newCompany.Name = company.Name;
  16:          newCompany.TimeZone = company.TimeZoneID;
  17:          Database.Companies.InsertOnSubmit(newCompany);
  18:          Database.SubmitChanges();
  19:      }
  20:   
  21:      public Model.Company GetCompanyByName(Guid clientID, string name)
  22:      {
  23:          return MapCompany(Database.Companies.SingleOrDefault(c => c.FKClientID == 
  24:              clientID && c.Name.Trim().ToLower() == name.Trim().ToLower()));
  25:      }
  26:   
  27:      private static Model.Company MapCompany(Company source)
  28:      {
  29:   
  30:          if (source == null)
  31:              return null;
  32:   
  33:          var company = new Model.Company();
  34:          company.ClientID = source.FKClientID;
  35:          company.Created = source.Created;
  36:          company.ID = source.ID;
  37:          company.Language = (Model.Language) source.Language;
  38:          company.Name = source.Name;
  39:          company.TimeZoneID = source.TimeZone;
  40:   
  41:          return company;
  42:   
  43:      }
  44:   
  45:  }

Wie leicht zu sehen ist, passiert hier außer ein bisschen Mapping nicht viel. Dennoch treibt mich seit Längerem die Frage um, ob ich nicht auch meine Repositories testen sollte. Also testen im Sinne von Test-first und test-getriebener Entwicklung. Ich tue mich mit dieser Frage ausgesprochen schwer.

Dafür spräche, dass es hin und wieder vorkommt, dass in der Anwendung, die, wenn man den Testrunner laufen lässt, 100% grün ist, später im Browser Fehler auftreten. Weil z.B. irgendwo ein Feld verwendet wird, welches im Test des Service, in dem das Repository gemockt ist, einen Wert aufweist, in der Praxis aber nicht, weil schlicht das Mapping fehlt (vergessen wurde). Oder weil Inserts plötzlich nicht mehr klappen, weil die ID nicht gesetzt wurde und so versucht wird mehrfach 0 (Guid.Empty) hinzuzufügen.

Dagegen spricht der enorme Aufwand. Denn eines ist ja mal klar: 100% Test-Abdeckung kann nicht das Ziel sein, wenn man nicht seine Produktivität völlig opfern möchte. Denn wenn man jeden Schnipsel test-first entwickelt, anschließend noch Integrations- und Akzeptanztests durchführen soll, wird man schlicht nie fertig. Und wenn man in diesem Fall sinnvollerweise den ORM nicht wegmockt sondern eine echte Datenbank mit echten Testdatensätzen verwendet, muss man auch noch das Schema (und die Daten!) dieser Test-Datenbank ständig aktuell halten, was bei einer iterativen Entwicklung äußerst schwer fällt (und auf die Nerven geht).

Ich habe mich bei dieser Gelegenheit an eine Diskussion und einen Blog-Beitrag von Steven Sanderson vom letzten Jahr erinnert, in dem er ein ganz treffendes Schaubild brachte, wie ich finde:

Meine Meinung: Code wie oben gehört für mich in die rechte untere Ecke. Im Prinzip koordiniert das Repository nur das Mapping der Daten vom ORM zu meinen Objekten (Entities, Value Objects) und umgekehrt. Man kann (in den allermeisten Fällen) auf einen Blick erkennen, was eine Methode tut und es würde keinen wirklichen Nutzen bringen, diese Einzeiler zu testen.

Daher mein (vorläufiges) Fazit, auch geklaut bei Steven:

Your time is finite; spend it more effectively elsewhere.

Disclaimer:

Dies gilt ausdrücklich für die Kombination Repository + Linq to SQL/ORM und ist natürlich nicht allgemein gültig. Ich habe z.B. einen Fall einer Anwendung, in der ich innerhalb der Repositories auf ein REST-API zum Lesen und Schreiben zugreife. Hier war die test-getriebene Entwicklung mehr als hilfreich, gerade auch für die Wartung (als sich das API z.B. einmal ohne Vorankündigung änderte war die Ursache dank der Integrationstests schnell ausgemacht).

Kommentare

  1. Ken schrieb am Dienstag, 27. April 2010 01:49:00 Uhr:

    Für so eine triviale Geschichte, ist TDD für die Repositories fehl am Platz, da geb ich dir Recht.
    Spannend wird es allerdings, wenn nach diversen Regeln gefiltert bzw. sortiert und evtl. auch noch Daten aus anderen Tabellen geladen werden sollen. Dann haben Tests in meinen Augen auch für die Repositories eine Daseinsberechtigung.

    Bsp: Hole mir alle Unternehmen und zeige mir die Angestellten, deren Name mit A beginnt und die nach dem 01.01.2010 erstellt wurden.

    Wie gehst du hier ohne Tests sicher, dass dir dein Repository die richtigen Daten liefert bzw. den Query richtig zusammenbaut und ausführt?
    Mit einem Test abgedeckt sein müsste doch: Wenn als Filter "Name beginnt mit A" kommt, dann erweitere den Query um die entsprechende Anweisung.

    Mittlerweile bin ich auf dem Standpunkt, dass man pro Repository etwas Zeit für den ein oder anderen Integrationstest spenden sollte. An dem Bauen des Queries und dem Mapping ändert sich ja nicht sonderlich viel (solange keine Änderungen an Datenbank, Business Model oder Filter notwendig werden)...zur Zeit wird bei uns der Test nach erfolgreicher Durchführung auskommentiert, da wir uns noch keinen Kopf um die Initialisierung und Bereinigung einer Test-Datenbank gemacht haben.
  2. Stefan Lieser schrieb am Dienstag, 27. April 2010 08:33:00 Uhr:

    Hallo Thomas,

    es scheint mir so, dass das eigentliche Problem woanders liegt. Du schreibst, das Generieren des Schemas und das Befüllen der DB mit Testdaten sei aufwändig. Da gebe ich dir prinzipiell Recht. Allerdings hängt das auch vom Werkzeug ab. Wenn LINQ to SQL nicht in der Lage ist, aus den Mappings das Schema zu generieren, liegt hier ein Problem. Und wenn du Testdaten in der DB nicht mit Hilfe der gemappten Objekte erzeugen kannst, ist das ein weiteres Problem.

    Bei Verwendung von NHibernate erstelle ich mit Fluent NHibernate die Mappings. Dabei werden Konventionen verwendet, so dass dies schnell von der Hand geht und vor allem leicht änderbar ist. Als nächstes erstelle ich, ebenfalls mit Fluent NH, Tests welche das Mapping gegen eine DB testen. Damit stelle ich sicher, dass alle relevanen Felder der Objekte korrekt persistiert werden. Auf dieser Basis kann ich dann aufsetzen bei Repository Tests. Die Testdaten dazu erzeuge ich nämlich mit den gemappten Objekten. Ich weiß ja aus den Mappingtests, dass die Objekte korrekt persistiert werden. Also lege ich Testdaten auf diesem Weg an, instanziere ein Repository und teste es. Also ist das Erzeugen einer leeren DB (Schema kommt von NHibernate) und Anlegen von Testdaten kein Problem.

    Ob und wie das alles mit LINQ to SQL geht kann ich nicht sagen. Aber sollte es damit nicht so einfach gehen, wäre das ein Grund für mich, entweder das notwendige Tooling zu ergänzen oder zu einem anderen OR Mapper zu wechseln.

    Viele Grüße
    Stefan

    P.S. Die Vorgehensweise kann in der databasepro in 2 Artikeln nachgelesen werden.
  3. Thomas schrieb am Dienstag, 27. April 2010 11:02:00 Uhr:

    @Ken:

    Klar, je komplexer irgendetwas wird, desto sinnvoller sind automatisierte Tests. Letztendlich könnte man ja schon im Beispiel bei GetCompanyByName() testen, ob Eine Firma "miCrOsOft" gefunden wird, obwohl in der Datenbank "Microsoft" steht.

    Die Frage ist nur: wo fange ich an bzw. wo höre ich auf. Welchen Vorteil bringt mir ein entsprechender Unit- bzw. Integrationstest? Vor allem im Gesamtkontext: das Repository ist ja nur ein Teil des Ganzen, d.h. die darüber sitzende Logik wird ja test-getrieben entwickelt und das UI in Form eines (wie auch immer gearteten) Akzeptanztests ebenfalls getestet. Spätestens hier wird dann auch das Verhalten des Repositories mitgetestet.

    Schwierig, schwierig. Ich bin hin- und hergerissen :-). Muss mir wohl mal die Artikel von Stefan durchlesen und einen Blick auf nhibernate werfen.
  4. Alex schrieb am Dienstag, 27. April 2010 16:53:00 Uhr:

    NHibernate: +1 ;-)
  5. Thomas schrieb am Dienstag, 27. April 2010 20:18:00 Uhr:

    Nee, keine Lust auf NHibernate. Mag praktisch, mächtig und schön sein, aber für reines CRUD gekapselt im Repository ist mir das viel zu viel Overhead, den ich mir mit Linq to SQL sparen kann.

    Stefan hat mich aber auf den richtigen Pfad gebracht. Ich bin gedanklich bisher davon ausgegangen, die Testdatenbank manuell und parallel pflegen zu müssen, das ist aber nicht der Fall. Ich blogge dazu demnächst etwas mehr.
  6. Alex schrieb am Dienstag, 27. April 2010 20:47:00 Uhr:

    Ich habe die DB früher auch "von Hand" für Tests präpariert:
    http://blog.alexonasp.net/post/2008/10/11/Datenbank-fuuml3br-Unit-Tests-dynamisch-erzeugen-und-mit-Daten-fuuml3bllen.aspx
  7. Thomas goes .NET schrieb am Samstag, 1. Mai 2010 19:11:00 Uhr:

    Neulich bin ich nach langem Grübeln und einigem Hin- und Her für mich


« Zurück  |  Weiter »