In dem letzten UnitTest HowTo haben wir den ersten Schritt in Richtung "bessere Software" getan.
Was bei Unit Tests beachtet werden sollte: Ein Unit Test soll genau eine Sache testen - abgeschottet von allen anderen Sachen.
Phil Haack hat einen sehr guten Blogpost darüber geschrieben (und noch einen anderen ;) ) - der auch mich etwas "wachgerüttelt" hat.
Was bedeutet das:
Wenn wir angenommen eine 3-Schichten Architektur haben und unseren Service Layer testen möchten, sollten wir tunlichst vermeiden über den Data Access Layer zu gehen und so direkt zur DB zu reden. Die DB kann sich verändern!
Warum?
Wenn man direkt mit der DB redet, muss man sich auf bestimmte Daten verlassen - d.h. statische Daten laden in den Tests. Falls die Daten in der DB mal verloren gehen, gehen leider die UnitTests nicht mehr (selber in einem Projekt soeben bemerkt ;) ).
Ich mach eine Testdatenbank!
Man kann natürlich eine direkte Testdatenbank habe - allerdings ist es dann trotzdem recht schwierig eine "valides" Umfeld bei jedem einzelnen Test zu haben. Oder man resettet nach jedem Test die TestDB - allerdings laufen dann die Unit-Tests sehr zäh ab und es ist auch recht schwierig zu managen.
Mein Idealbild:
Bei jedem Unit Test möchte ich meine Umgebung exakt auf den Testfall konstruieren - sodass ich genau das Verhalten testen und daraufhin implementieren kann.
Das Zauberwort: Mocking
Durch den Einsatz von Schnittstellen usw. kann man natürlich überall Fake / Dummy Klassen erstellen - wie z.B. ich es hier getan hab mit den statischen Daten. Darunter kann man schonmal grob Mocking verstehen.
Allerdings ist es ziemlich umständlich für alles und jeden Test die Umgebung entsprechend durch solche Fake-Implementationen komplett zu mimen. Zudem fehlt etwas die Verbindung zwischen der Mockklasse und dem tatsächlichen Einsatzort.
Die Magie der Frameworks
Wie für fast jedes Problem, gibt es auch hier kleine Helferlein, welche durch Magie (es ähnelt Magie ;) ) dir den kompletten Implementationsaufwand der einzelnen konkreten Klassen abnehmen.
Es gibt in der .NET Welt einige Mocking Frameworks: Moq, Rhino.Mocks, TypeMock usw.
Ich bezieh mich nun auf Rhino.Mocks.
In die Praxis:
Wir haben wieder den ähnlichen Aufbau wie die letzten Male:
Wer genauer hinschaut, sieht, dass es keine konkrete Implementation von IPersonRepository zu sehen ist - keine DummyRepository, auch nicht im Test.
Der Aufbau:
In unserem "PersonFilter" haben wir diesmal nur den "WithAge" Filter. Unser "IPersonRepository" hat eine "GetPersons" Methode.
Den PersonService haben wir nun so implementiert:
namespace RhinoMocks.Services { public class PersonService { private IPersonRepository PersonRep { get; set; } public PersonService(IPersonRepository rep) { this.PersonRep = rep; } public IList<Person> GetPersonsByAge(int age) { return this.PersonRep.GetPersons().WithAge(age).ToList(); } } }
Im Konstruktur übergeben wir unser PersonRepository und in der GetPersonsByAge Methode fragen wir quasi über unseren Filter das entsprechende Repository ab.
Ich benutze die Rhino.Mocks Version 3.5 - welche die aktuellste ist. Diese einfach referenzieren.
Im Test möchten wir nun genau diese eine Methode testen:
[TestMethod] public void PersonService_GetPersonByAge_Works() { MockRepository mock = new MockRepository(); IPersonRepository rep = mock.StrictMock<IPersonRepository>(); using (mock.Record()) { List<Person> returnValues = new List<Person>() { new Person() { Age = 11, Name = "Bob" }, new Person() { Age = 22, Name = "Alice" }, new Person() { Age = 20, Name = "Robert" }, new Person() { Age = 40, Name = "Hans" }, new Person() { Age = 20, Name = "Peter" }, new Person() { Age = 20, Name = "Oli" }, }; Expect.Call(rep.GetPersons()).Return(returnValues.AsQueryable()); } using (mock.Playback()) { PersonService service = new PersonService(rep); List<Person> serviceResults = service.GetPersonsByAge(20).ToList(); Assert.AreNotEqual(0, serviceResults.Count); foreach (Person result in serviceResults) { Assert.AreEqual(20, result.Age); } } }
Achtung: Wie es genau funktioniert wieß ich auch noch nicht - allerdings tut es ;)
Schauen wir es uns mal genauer an:
MockRepository mock = new MockRepository(); IPersonRepository rep = mock.StrictMock<IPersonRepository>();
Dieser Codeschnipsel erstellt uns dynamisch ein Objekt vom Typ "IPersonRepository".
Jetzt kommt der "Record" Mode:
using (mock.Record()) { List<Person> returnValues = new List<Person>() { new Person() { Age = 11, Name = "Bob" }, new Person() { Age = 22, Name = "Alice" }, new Person() { Age = 20, Name = "Robert" }, new Person() { Age = 40, Name = "Hans" }, new Person() { Age = 20, Name = "Peter" }, new Person() { Age = 20, Name = "Oli" }, }; Expect.Call(rep.GetPersons()).Return(returnValues.AsQueryable()); }
Alles was innerhalb von diesem Using ist, wird "aufgezeichnet". Als erstes erstelle ich mir meine PersonCollection (mein Umfeld in dem in den Test laufen lassen möchte).
In "Expect.Call" geb ich die Methode an, die aufgerufen wird und was dabei als Returnvalue zurückgegeben wird (in meinem Fall meine PersonCollection).
Im letzten "Playback" Abschnitt:
using (mock.Playback()) { PersonService service = new PersonService(rep); List<Person> serviceResults = service.GetPersonsByAge(20).ToList(); Assert.AreNotEqual(0, serviceResults.Count); foreach (Person result in serviceResults) { Assert.AreEqual(20, result.Age); } }
Hier wird das, was wir vorher definiert haben bei den entsprechenden Calls "abgespielt".
Wie die Magie direkt funktioniert, weiß ich nicht, allerdings wird dynamisch zur Laufzeit dieser Typ generiert:
Ergebnis:
Der Test läuft durch und ich hab durch diesen Mock genau nur eine Sache getestet - ob der Service läuft oder nicht. Ob die DB verrückt spielt ist mir an dieser Stelle egal :)
Andere Beispiele:
Ich hab am Anfang ein einfaches "Hello World" Beispiel gefunden. Einige Screencasts findet man hier und ein kleines Wiki hat auch noch ein paar Artikel darüber.
Wenn mein Beispiel komplett falsch ist (oder ich eine falsche Erklärung hab), dann einfach melden - ich arbeite mich erst in diese Materie ein :)