Im November 2009 haben Peter Bucher und ich uns im Rahmen unseres monatlichen Streitgesprächs mit der Frage Wie viel Sinn machen Unittests? beschäftigt. Meine damals noch sehr skeptische Haltung ist inzwischen einer regelrechten Begeisterung für Unittests gewichen.
Auch Kirchen haben Kragsteine – zu dieser Einsicht hat mir Ralf Westphal verholfen, wodurch meine mentale Barriere in Bezug auf Unittests gelöst war und ich begonnen habe, mich tiefer mit der Materie zu beschäftigen. Erstes Ergebnis war mein Vorschlag, wie ein Ordnungssystem für Unittests aussehen könnte.
Seither ist nun ein Vierteljahr vergangen, in welchem ich mich viel mit dem tatsächlichen Schreiben von Unittests befasst habe. Auch Themen wie Stubs und Mocks wie auch der innere Aufbau von Tests an Hand des Arrange-Act-Assert-Musters (AAA) standen auf der Tagesordnung.
Dabei hat mir vor allem der Austausch mit anderen Entwicklern geholfen, doch auch das Buch Pragmatic Unit Testing konnte mir diverse Fragen und Unklarheiten beantworten, weshalb ich dieses Buch jedem an der Thematik Interessierten nur empfehlen kann.
Heute hat mich nun Roberto Bez mit einer interessanten Frage konfrontiert: Wenn ein einzelner Unittest nur einen einzelnen Aspekt testen soll, wie wird dann gesichert, dass die Vorbedingungen ihrerseits nicht fehlschlagen?
Unser Diskussiongegenstand war ein Unittest, der einen Datensatz in einer Datenbank löschen soll – dazu aber aus ebendieser zunächst gelesen werden muss. Dass das eigentliche Löschen in die Assert-Phase fällt, liegt auf der Hand – doch was, wenn bereits das Lesen fehlschlägt?
Meines Erachtens fällt das Lesen noch in die Arrange-Phase des Unittests, denn es dient der notwendigen Vorbereitung für den eigentlichen Test: Schließlich soll der Unittest das Löschen des Datensatzes testen – und nur das Löschen!
Alle Vorbedingungen, die erfüllt sein müssen, dürfen zwar ebenfalls nicht fehlschlagen, gehören aber nicht direkt zu dem getesteten Aspekt:
[TestFixture]
public class WhenDeleteIsCalled
{
[Test]
public void WithAnExistentRecord_TheRecordIsDeleted()
{
// Arrange.
var input = this._repository.ReadRecord(this._id);
var expected = this._repository.ReadRecords().Count - 1;
// Act.
this._repository.Delete(input);
var actual = this._repository.ReadRecords().Count;
// Assert.
Assert.That(actual, Is.EqualTo(expected));
}
}
Was also, wenn bereits das Lesen des Datensatzes fehlschlägt?
Naheligend wäre, auch das Lesen in die Assert-Phase zu ziehen, doch dies würde eine Vermischung von Vorbereitung und tatsächlicher Durchführung bedeuten. Ruft man sich zudem in Erinnerung, dass ein einzelner Unittest nur einen einzelnen Aspekt testen soll, wird klar, dass dies keine Lösung darstellt.
Um einen besseren Ansatz zu finden, muss man sich zunächst zwei Fakten vor Augen führen:
- Ein fehlgeschlagener Unittest liefert nur in den seltensten Fällen auf den ersten Blick den Grund, warum etwas fehlgeschlagen ist – in der Regel steht nur die Information zur Verfügung, dass etwas fehlgeschlagen ist, so dass eine weitere Analyse erforderlich ist.
- Neben dem exemplarisch genannten Unittest existieren weitere Unittests in dem Projekt – unter anderem auch solche, die das Lesen von Datensätzen testen.
Tritt also bereits beim Lesen des Datensatzes ein Problem auf, so schlägt nicht nur der Unittest fehl, der das Löschen abdeckt, sondern auch jener, der bereits das Lesen testet. Welcher von beiden in der zeitlichen Reihenfolge dabei zuerst ausgeführt wird, darf keine Rolle spielen, da Unittests per Definition unabhängig voneinander gestaltet werden müssen.
Nun stellt sich also die Frage – welcher Unittest wird zuerst begutachtet? Wird zuerst der Unittest analysiert, der das Lesen abdeckt, wird der Fehler direkt gefunden und behoben. Im nächsten Durchlauf wird dann auch der Unittest, der das Löschen abdeckt, erfolgreich durchgeführt.
Wird hingegen zuerst der Unittest analysiert, der das Löschen abdeckt, so stößt man während der Analyse auf den Punkt, dass das Problem eben nicht in der Act- oder Assert-, sondern bereits in der Arrange-Phase auftritt. An dieser Stelle wird klar, dass es einen anderen Test geben muss, der diese Funktionalität abdeckt, so dass der Fehler nun dort gesucht werden kann.
Es spricht im Hinblick auf die Fehlersuche also nichts dagegen, Funktionalität als Vorbedingung für andere Funktionalität zu nutzen – allerdings liegt ebenfalls auf der Hand, dass ein solches Vorgehen problematisch sein kann. Dann nämlich, wenn auf diese Art gegenseitige Abhängigkeiten von Funktionalitäten entstehen.
Wenn sich zwei Bereiche gegenseitig bedingen, kann es aufwändig werden, den eigentlichen Fehler zu finden, ohne sich ständig im Kreis zu drehen. Einen äußerst interessanten Ansatz, wie man diesem Problem begegnen kann, hat Ralf Westphal in Zustand als Abhängigkeit beschrieben.