Unit-Testing – Sebastians Blog https://sgaul.de Neues aus den Softwareminen Thu, 05 Mar 2015 18:59:00 +0000 de-DE hourly 1 https://wordpress.org/?v=6.1.7 https://sgaul.de/wp-content/uploads/2019/02/cropped-sgaul-2-1-32x32.jpg Unit-Testing – Sebastians Blog https://sgaul.de 32 32 Kreative Rails-Environments https://sgaul.de/2015/03/05/kreative-rails-environments/ Thu, 05 Mar 2015 18:59:00 +0000 https://sgaul.de/?p=2738 Kreative Rails-Environments weiterlesen]]> Rails-Environments sind mächtig, einfach einzurichten und schnell zu wechseln. Fast zu schade sich auf die Klassiker Test, Development und Production zu beschränken. Auch nach zehnmaligem Ändern der Mail-Einstellungen kam bei uns niemand auf die Idee, dass hier eine zweite Development-Umgebung nach Aufmerksamkeit schreit.

Mit erweitertem Horizont sind viele mehr oder weniger nützliche Umgebungen denkbar:

  • Development mit Mail-Versand um Mails im Mailprogramm zu testen
  • Development mit präkompilierten Assets
  • Development mit Production-Datenbank für Notfall-Debugging
  • Testing ohne Class-Cache um in einer Capybara-Session etwas anzupassen
]]>
Ruby: Testvollständigkeit testen https://sgaul.de/2014/05/20/ruby-testvollstaendigkeit-testen/ Tue, 20 May 2014 14:23:59 +0000 https://sgaul.de/?p=2679 Ruby: Testvollständigkeit testen weiterlesen]]> Eine Wissenschaft für sich im Test-Driven Development ist die Frage, wie jede einzelne Funktion getestet werden muss: Sind die Tests vollständig, hat man jeden Spezialfall bedacht? Hat man es zu gut gemeint und eine schwer überschaubare, redundante Testsuite geschaffen?

Einen recht ungewöhnlichen Weg zur Beantwortung der ersten Frage geht Markus Schirp. Sein Gem Mutant betrachtet Code und die zugehörigen, grünen Tests. Es verändert nun immer wieder bestimmte Teile des Applikations-Codes (nicht die Tests) und prüft, ob die Tests dadurch fehlschlagen. Tun sie dies nicht lässt sich schlussfolgern, dass diese Stellen des Codes nicht vollständig durch Tests abgedeckt oder tot sind.

Die Code-Änderungen sind dabei vollautomatisch. Ifs werden durch Unless ersetzt, boolsche Default-Werte von Argumenten werden negiert oder ganze Statements weggelassen. Solange die Tests dadurch fehlschlagen ist es egal wie viel Unsinn sich hieraus ergibt.

Der Ansatz klingt auf jeden Fall interessant und erscheint um einiges aussagekräftiger als die üblichen Code-Coverage-Statistiken. Ob er wirklich praxistauglich ist kann ich aufgrund mangelnder Erfahrung nicht beurteilen. Im Weg steht leider auch der etwas sperrige Einstieg. Ich konnte etwa keine Beispiele finden wie man Code mit Rails-Abhängigkeiten testet – und von diesem Code habe ich eine Menge. Ein Gem Mutant-Rails gibt es zwar schon, ist aber mit einem entmutigenden „This gem does not yet work“ überschrieben. Aber der Kurs scheint zu stimmen.

Auf der Rails-Conf 2014 gab es einen Vortrag zu Mutant. Dieser kratzt leider nur an der Oberfläche, zeigt aber immerhin ein Live-Beispiel.

]]>
Was beim Unit-Testing getestet werden sollte https://sgaul.de/2014/05/10/was-beim-unit-testing-getestet-werden-sollte/ Sat, 10 May 2014 20:30:21 +0000 https://sgaul.de/?p=2634 Was beim Unit-Testing getestet werden sollte weiterlesen]]> 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.

]]>