Was beim Unit-Testing getestet werden sollte

Die zentrale Idee des Unit-Testings ist Isolation. Man testet einzelne Komponenten, Abhängigkeiten werden als gegeben und korrekt angesehen und ggf. gemockt. Dies macht das  Testen der einzelnen Komponente übersichtlicher, da nur ihre Pfade betrachtet werden müssen. In der Regel wird hierbei ein Blackbox-Ansatz verfolgt. Die zu testende Funktionalität (meist eine öffentliche Methode) wird nur von außen betrachtet: Was geht rein, was kommt raus, wie ändert sich der nach außen sichtbare Zustand?

Es gibt hilfreiche theoretische Betrachtungen, welche Funktionen getestet werden sollten und welche nicht. Zudem lässt sich argumentieren, wie ein bestimmer Funktionstyp zu testen ist. Dies kann helfen den Aufwand des Testschreibens zu reduzieren. Zudem sorgt die bessere Isolation dafür, dass eine Änderung in einer Komponente nicht länger das Umschreiben hiervon eigentlich unabhängiger Tests erfordert.

Query und Command

Um zu formalisieren was getestet werden sollte, unterscheidet man zwei Arten von Funktionen: Query-Methoden geben einen Wert zurück, ändern aber nicht den Zustand des Systems (Array#length). Command-Methoden sind das entsprechende Gegenteil. Sie ändern den Zustand der Komponenten oder des Systems, haben aber keinen Rückgabewert (Array#push). Es gilt als guter Stil diese Trennung zu achten, auch wenn es zahlreiche akzeptierte Ausnahmen gibt (Array#pop, ActiveRecord::Base#save uvm.).

Nachrichten

Beim Testen betrachtet man Nachrichten, die die zu testende Komponente erhält oder versendet. Eingehende Nachrichten erteilen der Komponente den Auftrag etwas zu ändern (Command) oder zurückzugeben (Query). In der objektorientierten Welt ist dies der Aufruf einer Methode des zu testenden Objekts von außen. Der zugehörige Test stellt sicher, dass die Query-Methode den korrekten Wert zurück gibt und die Command-Methode die gewünschte Zustandsänderung bewirkt.

Ausgehende Nachrichten treten auf, wenn die zu testende Methode eine Methode eines anderen Objekts aufruft. Schickt eine Command-Methode eine solche Nachricht, muss sichergestellt werden, dass diese auch wirklich verschickt wird. Dies lässt sich komfortabel durch Mocks lösen. Ausgehende Nachrichten einer Query-Methode werden nicht getestet. Treten hier Probleme auf, werden diese durch die Tests der eingehenden Nachrichten deutlich.

Interne Nachrichten beschreiben den Aufruf einer Methode durch eine Methode des selben Objekts. Dies sollte in aller Regel nicht getestet werden, da die beiden vorigen Nachrichtentypen-Tests bereits sicherstellen, dass die Komponente korrekt funktioniert. Dies impliziert, dass auch interne Aufrufe korrekt sind. Solche Tests sind nicht nur unnötig, sondern fixieren zusätzlich Implementierungsdetails und behindern somit ein mögliches Refactoring.

Beispiel

Das folgende Beispiel zeigt mögliche Tests der Klasse Array. #length ist eine Command-Methode, bei der nur das Ergebnis wichtig ist. Bei #push zählt nur, dass das eingefügte Element Teil des Arrays wird. Für #map muss nur sichergestellt werden, dass die angegebene Methode auf dem Array-Element aufgerufen wird:

Nachrichtentyp Query Command
Eingehend
a = [1, [2, [3]]]
expect(a.flatten).to(
  eq [1, 2, 3]
)
a = [1]
a.push(2)
expect(a).to include 2
Ausgehend keine Tests
mock = Object.new
array = [mock]
mock.should_receive(:foo)
array.map(&:foo)
Intern keine Tests keine Tests

Tabelle in Anlehnung an The Magic Tricks of Testing by Sandi Metz

Beim Flatten-Beispiel erscheint es verlockend zu testen, dass auch auf den Elementen des Arrays flatten aufgerufen wird, wenn diese wiederum Arrays sind. Tatsächlich wird dieses rekursive Verhalten aber bereits vom Test der eingehenden Nachricht sichergestellt, ein Test der ausgehenden Nachricht der Query-Methode ist somit unnötig.