Buch-Rezension zu „The Art of Unit Testing (2nd Edition)“

The Art of UnitTesting 2nd editionSeit Ende letzter Woche gibt es mit „The Art of Unit Testing, Second Edition“ eine neue Ausgabe meines favorisierten TDD-Buches. Die erste Ausgabe erschien 2009 und beeinflusste mein Verständnis von Test-Driven Development nachhaltig.

Ich war einerseits gespannt auf die neue Ausgabe und andererseits auch ein wenig unentschlossen ob es wirklich eine neue Ausgabe benötigt. Konnte die neue Ausgabe die kleineren Mankos der 1. Ausgabe wirklich beheben? Oder gibt es am Ende ein ganz anderes Buch das nur den gleichen Namen trägt?

 

Aufbau beibehalten, Inhalt überarbeitet

Die Kapitelstruktur der 2. Ausgabe unterscheidet sich nur minimal von der 1. Ausgabe. Der ehemalige Anhang A „Design and testability“ ist nun ein „richtiges“ Kapitel und Mock-Frameworks werden in einem zusätzlichen Kapitel noch eingehender angeschaut. Die übrigen Kapitel blieben alle erhalten und wer sich nur am Inhaltsverzeichnis orientiert wird diese 2. Ausgabe wohl schnell wieder weglegen.

Die grossen Neuerungen finden sich in den Kapitel selber. Diese wurden komplett überarbeitet und liefern nun eine noch bessere Erklärung wieso man seine Unit Tests auf diese Art organisieren und strukturieren soll. Die Beispiele sind nun so gut dass man dieses Buch auch problemlos einem Anfänger geben kann. Roy Osherove führte zwischen den 2 Büchern zahlreiche TDD-Kurse durch und hat seine dabei gewonnenen Erkenntnisse in die 2. Ausgabe einfliessen lassen. Dadurch wurde auch hinsichtlich der Praxistauglichkeit nochmal ein Schritt nach vorne gemacht, was sich vor allem an den viel klareren Namensregeln zeigt.

Wenn man die erste Ausgabe kennt und täglich mit Unit Tests arbeitet wird man fast nur bei den Mock-Frameworks neue Erkenntnisse bekommen. Die aktuelle Liste lieferte einem eine gute Übersicht und kann einem auch helfen sich gegen ein bestimmtes Framework zu entscheiden:

MSFakes might be free and included with Visual Studio, but it will cost you a lot of money down the line in developer hours, fixing and trying to understand your tests.

Wer seine erste Ausgabe nicht nur als Staubfänger nutzt sollte sich die 2. Ausgabe zulegen. Ob man das Buch als Nachschlagewerk, zum Auffrischen seiner Unit Testing Kenntnisse oder für neue Teammitglieder braucht – die überarbeiteten Kapitel sind einfacher zu verstehen und näher an der Praxis.

 

Fazit

Mit der 2. Ausgabe konnte Roy Osherove die hohen Erwartungen übertreffen und ein noch besseres Buch über Unit Testing liefern. Die darin beschriebenen Vorgehensweisen helfen in der Praxis die Unit Tests im Griff zu behalten und eine Struktur hinein zu bringen. Wenn man die erste Ausgabe besitzt und damit noch arbeitet lohnt sich auch der Kauf der 2. Ausgabe.

 

Zum Buch

The Art of Unit Testing, Second Edition“ von Roy Osherove, November 2013 Manning, ISBN 978-1-6172-9089-3, 296 Seiten, Englisch

 

Die Testpyramide

Seit einigen Monaten stosse ich immer wieder auf das Konzept der Testpyramide. Ich finde dieses Bild sehr passen, da es die wesentlichen Aspekte auf den Punkt bringt. Um ein System wirklich zu testen gilt es mehrere Ebenen anzuschauen. Die Testpyramide zeigt diese auf und vermittelt auf eine leicht verständliche Weise wie sich die Anzahl der Testfälle staffeln soll:

Die Testpyramide

 

Unit-Tests als Grundlage

Unit-Tests bilden die Basis der Testpyramide. Die kleinstmöglichen Tests sollten sicherstellen dass das System im Kern funktioniert. Eine wichtige Eigenschaft von Unit-Tests: Sie sind Schnell. In wenigen Sekunden sollte man wissen ob es überhaupt Sinn macht die länger laufenden Tests zu starten. Diese Sekunden sind wohlgemerkt nicht für einen einzigen Tests gedacht, sondern für alle Unit-Tests zusammen – womit ein Unit-Test der länger als 1/100 Sekunde dauert schon als langsam gelten muss.

Damit Tests so schnell sind dürfen sie nur wenig testen. Weder eine Verbindung zur Datenbank noch ein Zugriff aufs Dateisystem oder Aufruf eines Webservices ist erlaubt. All diese Abhängigkeiten müssen entfernt werden. Ob dies mittels Konfiguration oder mit Mocks gemacht wird spielt dabei keine Rolle.

Die Geschwindigkeit alleine kann aber nicht das einzige Kriterium für einen Unit-Test sein. Sonst besteht die Testsuite am Ende nur aus leeren Methoden. Das was man testet soll auch noch Sinn machen. Und einem in die richtige Richtung weisen wenn einmal ein Test fehlschlägt. So ist man schnell einmal bei mehreren Kriterien die von Ben Rady und Rod Coffin in „Continuous Testing“ mit dieser Abkürzung zusammengefasst werden:

FIRE: Fast, Informative, Reliable and Exhaustive

 

Integrationstests

Zu wissen dass der eigene Code für sich alleine funktioniert ist ein Anfang. Damit weiss man aber noch nicht ob der Code auch mit anderen Teilen funktioniert. Hier kommen die Integrationstests ins Spiel.

Auf dieser Ebene werden all die Abhängigkeiten angeschaut die man bei den Unit-Tests entfernt hat. Was zuerst nach vermeidbarem Zusatzaufwand aussieht hat sehr wohl seine Berechtigung. Es genügt wenn man das Erzeugen, Speichern, Aktualisieren und Löschen eines Objekts in der Datenbank ein Mal pro Klasse testet. Dies hat die gleiche Aussagekraft (ist aber deutlich schneller) wie wenn man in allen Unit-Tests immer mit den Objekten aus der Datenbank arbeiten würde.

Da weniger Tests mit den Umsystemen nötig sind wirkt sich deren Ausführungsdauer nicht so stark auf die Länge des gesamten Testlaufs aus.

 

Akzeptanztests

Die Akzeptanztests bilden die Spitze der Testpyramide. Hier gilt es die Anwendung aus Sicht des Benutzers zu testen. Vom GUI durch die Geschäftslogik hin zur Datenbank und den externen Webservices soll hier alles geprüft werden.

Da man bereits weis das sowohl der Kern der Anwendung funktioniert und der auch mit den Umsystemen korrekt zusammenarbeitet benötigt man nur noch wenige Akzeptanztests. Diese dürfen noch einmal langsamer sein als die Integrationstests und sollen als letzte Stufe die Korrektheit der gesamten Anwendung belegen.

Und da es so wenige Tests sind kann man diese auch mit dem Kunden/Endbenutzer besprechen. Müssen wirklich nur die wichtigsten Tests angeschaut werden hat man gute Chancen dass dies auch wirklich gemacht wird.

 

Reihenfolge & Einschränkungen

Die Testpyramide gibt keine Reihenfolge für die Erstellung der Testfälle vor. Wenn es bei der Ausführung auch am meisten Sinn macht mit den Unit Tests zu beginnen so ist man beim Erstellen frei.

Hat man Glück und der Kunde will an Akzeptanztests mitarbeiten kann man einen Top-Down Ansatz wählen. Man beginnt mit einem fehlgeschlagenen Akzeptanztest und schreibt so lange Integrations- und Unit-Tests bis dieser erfüllt wird. Alternativ kann man aber auch mit den Unit-Tests beginnen und sich nach oben arbeiten.

Die Testpyramide ist aber nicht perfekt. Es gibt etliche Testarten die darin keinen Platz finden. Wo platziert man beispielsweise die Explorationstests? Oder die Performancetests? Trotz dieser Einschränkungen finde ich das Bild der Testpyramide sehr gelungen.

TDD: Denken erlaubt

Test-Driven Development (TDD) gibt auch heutzutage noch viel zu diskutieren. Was mir dabei immer wieder auffällt: Es scheint als ob vor lauter Red-Green-Refactor vergessen geht das man eigentlich Software entwickeln soll. Sobald man sich mit Tests beschäftigt vergisst man das grosse Ganze. Oder wieso schreiben gestandene Software-Entwickler einen Test nach dem anderen der ihnen nur bestätigt das 1 + 1 wirklich 2 ergibt?

 

Code Katas als Ursache?

Code Katas sollen einem dabei helfen sich bei einer Übung auf einen bestimmten Aspekt zu konzentrieren. Dieser Aspekt ist oft TDD, doch gibt es genügend andere Aspekte die man in den Vordergrund stellen kann (wie die Bedienung der IDE nur mit der Tastatur oder Patterns wie das Single Responsibility Principle).

Will man damit TDD erproben nimmt man in der Regel ein ganz einfaches Beispiel. Man will sich ja nicht lange mit dem Problem beschäftigen sondern TDD lernen. Entsprechend schreibt man viele Tests und denkt wenig über die Lösung nach – diese ist ja durch einfache Beispiele wie FizzBuzz gewollt. Man trainiert also ständig mit einfachen Beispielen und vielen Tests. Die Videos zu diesen Trainings zeigen entsprechend viele Tests und wenig Gedanken über die Lösung.

Kurzum: Der Feedback-Loop für den Entwickler zeigt ganz klar dass er TDD erst richtig macht wenn wer viel testet und wenig denkt. Das Problem ist nur das dies komplett falsch ist.

 

Software-Entwicklung vor TDD

Gehen wir einen Schritt zurück. Bevor man mit TDD Software entwickeln wollte machte man in der Regel diese Schritte:

  1. Problem analysieren
  2. Lösung erarbeiten
  3. Programmieren
  4. Testen
  5. Veröffentlichen
  6. Bugs fixen
  7. Veröffentlichen

Bei der iterativen Software-Entwicklung folgt man ebenfalls dieser Schrittfolge – einzig der Umfang behandelt nicht mehr das ganze System sondern nur einen Teil.

Die Idee von TDD war es ursprünglich einmal die Schritte 3 & 4 zu kombinieren. So hoffte man die teure Behebung von Bugs nach der Veröffentlichung zu minimieren. Nie war es das Ziel die Phase der Problemanalyse oder dem Erarbeiten der Lösung zu streichen.

 

Babysteps ins Chaos

Die Analyse der Problemstellung und das erarbeiten der Lösung ging irgendwo bei der Einarbeitung in TDD verloren. Irgendwann sollten Babysteps (wie 1+1 = 2) die Verständnislücken der Problemstellung füllen und die Lösung herausfallen. Nur wo passiert dies? Viel eher stehen all die minimalen Tests nur im Weg und erschweren die Weiterentwicklung der Software. Alleine mit Babysteps lösen wir das Problem nicht.

 

Schrittgrösse variieren

Wie aber geht man „richtig“ vor? Kent Beck zeigt in „TDD By Example“ wie man erst das Problem analysieren soll, eine Liste mit Testfällen (und noch anzugehenden Problemen) führt und vor allem wie man die Schrittgrösse variieren kann.

Das Red-Green-Refactor soll man beibehalten. Aber wenn man weiss wo hin man will und welche Lösung man verfolgt kann man grössere Schritte machen. Wie gross diese Schritte sind hängt vom konkreten Problem, der Vertrautheit mit der Technologie und dem angepeilten Lösungsweg ab.

Für den Lösungsweg greife ich oft auf Flussdiagramme zurück. So altmodisch die auch sein mögen, für eine grobe Skizzierung der Abläufe finde ich diese Art der Darstellung sehr hilfreich. Auch in der objektorientierten Programmierung gibt es unzählige Teilaufgaben die mit einem Flussdiagramm ideal abgebildet werden können.

Mit wenigen Worten pro Tätigkeit kann ich die einzelnen Kernaufgaben herausarbeiten. Ich sehe so auf einen Blick was alles zur Lösung dazu gehört und was es für Abhängigkeiten gibt. Passende Testfälle (und damit die Schrittgrösse) lassen sich so ebenfalls finden. Beim Programmieren tauchen dann immer noch genügend neue Testfälle auf. Diese können aber auch erst einmal nur auf eine Liste abgelegt und erst später priorisiert (oder gelöscht) werden.

 

Beispiele

Für das was ich bisher beschrieben habe gibt es zahlreiche gute Beispiele. Besonders empfehlen kann ich den Pluralsight-Kurs „Outside-In Test-Driven Development“ von Mark Seemann. Darin wird erklärt wie man mit TDD bei den Akzeptanztests beginnt und schrittweise verfeinert bis der notwendige Code geschrieben ist. Beginnt man mit den Akzeptanztests verliert man das eigentliche Ziel nicht aus den Augen. Das Mantra Red-Green-Refactor bleibt gültig ohne dass man sich in den Babysteps verliert. Und sind diese kleinen Schritte doch nötig hat man ein Rahmenwerk das einem daran erinnert wann genug ist.

Wer Bücher bevorzugt findet in „Rails 4 in Action“ von Ryan Bigg eine ausführliche Anleitung um testgetrieben eine Webanwendung zu erstellen. Hier wird noch mehr Wert auf die praktische Implementierung von TDD gelegt und gezeigt wie man mit unterschiedlichen Testebenen zu einer Lösung kommt.

 

Fazit

Nur weil man TDD macht muss man noch lange nicht aufhören selber zu denken. Eine Analyse der Lösung und ein grober Lösungsweg braucht es noch immer bevor man sich dem programmieren zuwendet. Verzichtet man auf diese grundlegenden Dinge schiebt man nur Code herum. Dies ist nicht nur ineffizient sondern auch teuer – sowohl beim erstmaligen schreiben wie auch bei all den kommenden Änderungen.

Daher unbedingt erst einmal abklären was man eigentlich machen will. Ist das „Was“ klar geht es ans „Wie“. Ob man dazu ein Flussdiagramm oder eine sonstige Skizze macht spielt keine Rolle – wichtig ist das man sich überlegt wie man vorgehen will. TDD und Red-Green-Refactor können viel besser, klarer und sinnvoller angewendet werden wenn man diese Vorarbeiten macht.

Moq für komplexere Anwendungsfälle

Moq ist eine kleine Mock-Bibliothek, die ich hier bereits einmal vorgestellt habe. Für die meisten Anwendungsfälle genügt es die Rückgabewerte von Methoden zu beeinflussen. Ab und zu wäre es aber praktisch ein wenig mehr mit den Mocks machen zu können. Heute zeige ich wie Moq einem bei spezielleren Anwendungsfällen unterstützen kann.

 

Flexible Rückgabewerte

Mit Moq ist es kein Problem verschiedene Rückgabewerte zu liefern – so lange sich die Parameter beim Methodenaufruf unterscheiden. Der notwendige Code um den Wert „A“ für die Id 1 und „B“ für die Id 2 zu erhalten sieht so aus:

public interface ISimpleDemo
{
    string GetValue(int id);
}

[TestMethod]
public void MultipleReturnsForDifferentInputs()
{
    var mock = new Mock<ISimpleDemo>();
    mock.Setup(service => service.GetValue(1)).Returns("A");
    mock.Setup(service => service.GetValue(2)).Returns("B");

    Assert.AreEqual("A", mock.Object.GetValue(1));
    Assert.AreEqual("B", mock.Object.GetValue(2));
}

Hin und wieder steht man aber vor der Aufgabe bei einer Methode ohne Parameter verschiedene Rückgabewerte definieren zu müssen. Der direkte Versuch die gewünschten Werte nach einander zu definieren schlägt leider fehl:

public interface IBlogDemo
{
    string GetNextName();
}

[TestMethod]
public void MultipleReturnsTheWrongWay()
{
    var mock = new Mock<IBlogDemo>();
    mock.Setup(service => service.GetNextName()).Returns("A");
    mock.Setup(service => service.GetNextName()).Returns("B");
    mock.Setup(service => service.GetNextName()).Returns("C");

    Assert.AreEqual("A", mock.Object.GetNextName()); // Gibt "C"
    Assert.AreEqual("B", mock.Object.GetNextName());
    Assert.AreEqual("C", mock.Object.GetNextName());
}

So überschreibt man nur den Rückgabewert und der Tests endet mit dieser Fehlermeldung:

Assert.AreEqual failed. Expected:<A>. Actual: <C>.

Damit man diesen Test erfüllen kann benötigt es einen anderen Ansatz. Mit Hilfe einer Queue kann man die Werte in der gewünschten Reihenfolge ablegen (auch null-Werte sind so möglich). Die Returns-Funktion von Moq ruft nun die Dequeue-Methode auf und bekommt die für den Test benötigten Werte zurück:

[TestMethod]
public void MultipleReturnsRight()
{
    var mock = new Mock<IBlogDemo>();
    var results = new List<string> {"A", "B", "C", null};
    var pq = new Queue<string>(results);

    mock.Setup(c => c.GetNextName()).Returns(pq.Dequeue);

    Assert.AreEqual("A", mock.Object.GetNextName());
    Assert.AreEqual("B", mock.Object.GetNextName());
    Assert.AreEqual("C", mock.Object.GetNextName());
    Assert.IsNull(mock.Object.GetNextName());
    // ==> Funktioniert
}

 

Wurde eine Methode aufgerufen?

Moq bietet einem die Möglichkeit einzelne Aspekte des verwendeten Mocks zu überprüfen. Muss man wissen wie oft eine Methode des Mocks aufgerufen wurde ist die Funktion Verify sehr hilfreich. Der notwendige Code um zu prüfen ob GetValue mit Parameter 1 genau einmal aufgerufen wurde wird so sehr einfach:

[TestMethod]
public void ExactlyOnceCalled()
{
    var mock = new Mock<ISimpleDemo>();

    mock.Object.GetValue(1);

    mock.Verify(db => db.GetValue(1), Times.Once());
}

Wird die Methode nicht mit dem erwarteten Parameter aufgerufen (zum Beispiel in dem man im oberen Code-Ausschnitt GetValue mit dem Wert 2 aufruft), wirft Moq eine entsprechende Fehlermeldung:

Test method TestProject1.UnitTest1.ExactlyOnceCalled threw exception:
Moq.MockException:
Expected invocation on the mock once, but was 0 times: db => db.GetValue(1)
No setups configured.
Performed invocations:
ISimpleDemo.GetValue(2)

Es kann vorkommen das man nicht genau weiss mit welchem Parameter die Methode aufgerufen wurde (wenn beispielsweise in der zu testenden Klasse neue Instanzen erzeugt werden). In diesem Fall kann man die It-Klasse verwenden:

[TestMethod]
public void ExactlyOnceCalledUnknownParameter()
{
    var mock = new Mock<ISimpleDemo>();

    mock.Object.GetValue(1);

    mock.Verify(db => db.GetValue(It.IsAny<int>()), Times.Once());
}

Auch bei der Anzahl der Aufrufe ist Moq sehr flexibel. Ob eine Methode einmal, nie, genau x mal oder innerhalb eines Bereiches aufgerufen wurde kann man alles mit der Times-Klasse überprüfen lassen:

[TestMethod]
public void TimesHasManyUsages()
{
    var mock = new Mock<ISimpleDemo>();

    mock.Object.GetValue(1);
    mock.Object.GetValue(2);
    mock.Object.GetValue(2);

    mock.Verify(db => db.GetValue(1), Times.Once());
    mock.Verify(db => db.GetValue(2), Times.AtLeastOnce());
    mock.Verify(db => db.GetValue(2), Times.Between(1, 2, Range.Inclusive));
    mock.Verify(db => db.GetValue(2), Times.Between(1, 3, Range.Exclusive));
    mock.Verify(db => db.GetValue(2), Times.Exactly(2));
    mock.Verify(db => db.GetValue(3), Times.Never());
}

 

Fazit

Moq unterstützt einem auch bei komplexeren Einsatzszenarien. Die Möglichkeiten von Verify sind vielfältig und können einem dabei helfen so manches Testszenario abzudecken. Dabei sind It.IsAny und Times eine grosse Hilfe.

Allerdings darf man bei so viel Flexibilität das Ziel des Tests nicht aus den Augen verlieren. Sobald man sich auf zu viele Implementationsdetails einlässt werden die Tests sehr schnell unwartbar. Im Zweifel sollte man daher lieber erst versuchen mit einer besseren Code-Struktur den Test einfacher zu gestalten.

Buch-Rezension zu „Professional Test Driven Development with C#“

Professional Test Driven Development with C#“ von Jeff McWherter und James Bender erschien im Mai 2011 bei Wiley. Das Buch richtet sich an all die Entwickler, die ihre C#-Anwendungen nach Test-First entwickeln wollen.

Wer sich bisher noch nicht mit dem Thema Test Driven Development (TDD) beschäftigt hat findet in den ersten beiden Teilen des Buches eine gute und ausführliche Einführung ins Thema.

 

 

TDD Szenarien aus der Praxis

Der dritte Teil widmet sich den praxisorientierten TDD Szenarien. Im Gegensatz zu vielen anderen Büchern geht es hier um die schwerer zu testenden Teile:

  • ASP.Net WebForms
  • ASP.Net MVC
  • JavaScript
  • WPF
  • Silverlight
  • WCF

Im Buch wird zu jeder dieser Technologien aufgezeigt wo die besonderen Herausforderungen liegen und wie man diese Testen kann. Wann immer möglich sollte man die dazu passenden Patterns verwenden. Geht dies nicht ist man auf sich alleine gestellt: Das Buch erklärt leider nur genau einen Weg um die entsprechende Technologie zu testen.

 

Werkzeuge und Katas

Als Abschluss gibt es einen Teil der sich den Werkzeugen widmet. Bei der Vielzahl möglicher Test-, Mock- und DI-Frameworks ist es nicht leicht eine Auswahl zu treffen. Die Autoren vergleichen jeweils einige Werkzeuge aus dem gleichen Bereich und nennen die für sie wichtigsten Unterschiede. Auch wenn dies die eigene Recherche nicht ersetzt, so ist dies doch ein guter Ausgangspunkt um seinen Werkzeugkasten zusammen zu stellen.

Im Appendix wir noch auf das Thema (TDD-) Katas eingegangen. Kleinen Übungen sollen einem dabei helfen das gelernte zu verinnerlichen. Die Idee stammt aus dem Kampfsport und hilft dort die richtigen Aktionen und Bewegungen zur richtigen Zeit zu machen. Übertragen auf TDD bedeutet dies: Hat man immer und immer wieder geübt wie man erst einen Test und dann den produktiven Code schreibt, geht dies in einem über und man wendet die gleiche Technik auch mit dem Code direkt vor einem an.

 

Ein E-Book aber kein PDF

Wiley bietet für dieses Buch eine E-Book Version an, aber leider nur in den Formaten ePub und mobi. Gerade wenn ein Buch viele Code-Beispiele hat habe ich neben der Kindle-Version sehr gerne noch ein PDF. Nicht nur sieht der Code dort meist besser aus, er lässt sich auch einfach kopieren. Mir schein als ob Wiley der Konkurrenz in diesem Bereich noch weit hinterher hinkt.

 

Fazit

Den beiden Autoren ist in einem oft behandelten Themenbereich ein gutes Werk gelungen. Die beiden Einführungsteile liefern alles nötige Grundwissen damit man auch als Anfänger durchstarten kann. Die Auswahl eines Werkzeugkastens und das vermitteln der Ideen rund um TDD runden dieses Buch ab. Mir fehlen hier einzig noch einige alternative Ansätze zum Testen der behandelten .Net-Komponenten.

The Art of Unit Testing“ ist zwar immer noch mein Favorit zum Thema TDD, dieses Buch folgt aber mit einem sehr kleinen Abstand.

 

Zum Buch

Professional Test Driven Development with C#“ von Jeff McWherter und James Bender, 2011 Wiley, ISBN: 978-0-470-64320-4, 360 Seiten, Englisch