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.