© saicle/Shutterstock.com
Enterprise-Softwarequalität im Umfeld dynamischer Sprachen

Cross-Plattform-Unit-Testing


Wenn Sie als Nutzer den Hinweis bekommen, dass für eine Software ein neues Update verfügbar ist, ist das meist ein Grund zur Freude. Neue Features, neues UI, alles wird besser. Allerdings zeigt die Erfahrung, dass neue Versionen auch meist neue Bugs bedeuten. Für Sie als Nutzer kann das in Frust enden. Im schlimmsten Fall suchen Sie sich einfach eine alternative Anwendung, die den gleichen Zweck erfüllt. Doch was können Sie – aus Sicht eines Softwareentwicklers – tun, damit bei Ihren Kunden bei Updates anstatt Unbehagen ein freudiges Gefühl ausgelöst wird?

Wir Entwickler sind Feuer und Flamme, wenn es darum geht, eine neue Software zu entwickeln. Neu! Neue Konzepte, neue Technologien, neues User Interface und gar eine neue User Experience! Eine Welt aus Zucker, in der alles bunt ist und schön glänzt. Was in der Vergangenheit war, kann außer Acht gelassen werden. Vielmehr hat man nur noch die Zukunft im Blick. Es ist an der Zeit, alles besser zu machen. Aus alten Fehlern zu lernen – um neue Fehler zu machen. Schnell ist der erste Prototyp entstanden, sind die ersten Ideen umgesetzt. Täglich kommen neue Features hinzu. Die Software entwickelt sich rasend schnell weiter. Doch nach einiger Zeit ist der Punkt erreicht, an dem die Entwicklung schwieriger wird. Plötzlich treten Seiteneffekte auf, die Anwendung wird instabil, und die Fehlersuche beginnt. Siehe da, es wird ein Fehler in einer alten Komponente gefunden, die lange niemand mehr angefasst hat. Es fiel bisher niemandem auf, dass sich seit Beginn der Entwicklung an dieser Stelle ein Bug befindet. Kaum ist der Bug gefixt, tritt ein zweiter in Abhängigkeit davon auf. Der Frust wird größer. Willkommen in der Welt der nicht getesteten Software!

Wie können wir als Softwareentwickler dieser Situation aktiv entgegenwirken? Wir könnten zum Beispiel jedes Release vor der eigentlichen Veröffentlichung selbst testen. Das kostet aber Zeit und Ressourcen. Das manuelle Testen wird von Version zu Version länger dauern und ist auch nur dann sinnvoll, wenn man immer exakt gleich vorgeht. Es gibt allerdings jemanden, der gerne Dinge schnell und wiederkehrend erledigt: unser Computer.

Arten von Tests

Wenn wir unserem schnellen Rechenkollegen einmal beigebracht haben, wie unsere Software getestet werden soll, wird er liebend gerne dafür sorgen, dass die Tests ausgeführt werden. Um ihm das Testen beizubringen, sollten wir uns erst einmal darauf verständigen, welche Arten von Tests es gibt – denn Softwaretests haben viele Namen und existieren in vielen Ausprägungen. Generell kristallisieren sich drei Gruppen von Tests heraus:

  • Unit Test (oder auch: Modultest, Komponententest): Es handelt sich hier um das isolierte Testen einer einzelnen Komponente (was meist exakt einer Klasse entspricht). Abhängigkeiten (z. B. der Zugriff auf die Festplatte oder das Absetzen von HTTP-Anfragen) innerhalb dieser Komponente werden durch so genannte Mocks ersetzt. Bei Unit Tests kann man vergleichsweise einfach positive und negative Fälle betrachten und testen.

  • Integrationstest: Ein Integrationstest testet das Zusammenspiel mehrerer Komponenten. Was zuvor im Unit Test als Mock abstrahiert wurde, kann nun wieder durch eine echte Komponente ersetzt werden (z. B. werden nun echte HTTP-Anfragen ausgeführt). Das Mocken von Komponenten ist bei Integrationstests dennoch erlaubt und erwünscht (oftmals verzichtet man hier beispielsweise auf das Testen des User Interface).

  • Systemtest (oder auch: Akzepttanztest, Acceptance Test, End-to-End-Test): Durch den Verzicht auf Mocks wird das komplette System in seiner Gesamtheit getestet. In diesem Fall werden oftmals nur Erwartungshaltungen getestet (also der positive Fall), da Fehlerfälle schon intensiv durch Unit und Integrationstests abgedeckt wurden.

Alle Arten von Tests haben eines gemeinsam: Sie sollen die Qualität, Pflege und Wartbarkeit von Code verbessern. Fehler sollen frühzeitig gefunden und behoben werden können, sodass sie erst gar nicht in die Produktion gehen und damit an den Kunden ausgeliefert werden.

Unit Tests

Speziell in diesem Artikel wollen wir uns das Thema Unit Tests genauer anschauen. Bevor die Frage nach dem „Wie“ beantwortet wird, wollen wir uns zuerst der Frage nach dem „Warum“ widmen. Warum möchte man Unit Tests durchführen? Warum einzelne Komponenten testen, wenn man auch direkt über einen Systemtest das gesamte System (und damit wieder einzelne Komponenten) testen kann? Eine berechtigte Frage. Oftmals lassen sich bei Systemtests manche Situationen nicht einfach nachspielen: ein Datenbankserver, der nicht erreichbar ist; das fehlerhafte Lesen von Daten oder das Zurücksenden vom falschen HTTP-Header. Stellen Sie sich vor, Sie müssten ständig die Internetverbindung trennen oder das LAN-Kabel ausstöpseln, um gewisse Situationen nachstellen zu können. Das würde wahrscheinlich dazu führen, dass Sie diese Tests nicht mehr machen würden.

Bei Unit Tests lassen sich solche Fälle oft sehr einfach nachstellen und wohldefiniert testen. Voraussetzung dafür ist allerdings eine saubere Architektur, die mittels Inversion of Control Container [1] und Dependency Injection externe Abhängigkeiten von außen an die Komponenten übergibt. Erzeugt jede Komponente ihre Abhängigkeiten selbst, haben Sie keine Chance, diese Abhängigkeiten für einen Test gegen Mocks auszutauschen.

Mocks sind Objekte, die als Platzhalter für echte Objekte eingesetzt werden. Man kann ihnen ein bestimmtes Verhalten geben, sodass sich viele Situationen simulieren und damit testen lassen. Sehen wir uns dazu Listing 1 an: Die TypeScript-Klasse TextReader verlangt beim Aufruf des Konstruktors ein Objekt der Klasse File­Reader. Der TextReader besitzt die Methode GetText. Beim Aufruf möchte die Methode einen Dateinamen filename wissen. Mit dem filename und dem fileReader liest die Methode die Datei ein und gibt sie als Text aus. Wenn wir nun davon ausgehen, dass wir immer einen validen Dateinamen übergeben, ist diese Methode sehr fehleranfällig:

  • Was passiert, wenn der Aufruf von fileReader.Get­File() einen Fehler wirft?

  • Was passiert, wenn der Aufruf von fileReader.Get­File() null zurückliefert?

  • Was passiert, wenn der Aufruf von file.ReadAsText() einen Fehler wirft?

Listing 1: Beispiel einer Komponente (Pseudocode)

class TextReader { constructor(FileReader fileReader); public string GetText(string filename) { var file = fileReader.GetFile(filename); var text = file.ReadAsText(); return text; } }

Selbstverständlich können Sie nicht von vornherein jeden Fall durch Tests abdecken. Viel wichtiger ist es, dass Sie nach Auftreten und Behebung eines Bugs einen Testfall dafür schreiben, sodass dieser in Zukunft nicht mehr auftreten kann.

rauber_unittests_1.tif_fmt1.jpgAbb. 1: Schematische Darstellung einer Isolation für einen Unit Test

Zwei gute Freunde: Jasmine und Karma

Wie in vielen Programmiersprachen üblich, existieren viele verschiedene Testframeworks, so auch für die HTML5-Cross-Plattform-Schiene. Unter JavaScript existieren Frameworks und Tools wie Jasmine [2], Karma [3], Mocha [4] oder QUnit [5]. Während Mocha und QUnit Testframework und Test-Runner in einem sind, beschränken sich Jasmine und Karma jeweils auf eine Rolle.

Jasmine ist ein Behavior-driven-Development-Testframework (BDD). Es ist unabhängig von Entwicklungsumgebungen oder der entwickelten Applikation und zielt darauf, auf jeder Plattform zu laufen. BDD bedeutet, dass es mithilfe einer Funktion describe() eine Testsuite, also eine zusammengehörige Gruppe von Tests, beschreibt. Jeder Test wird mit einer Methode it() individuell spezifiziert. Die Methode erwartet als ersten Parameter den Namen des Tests, der üblicherweise einen kompletten Satz darstellt und beschreibt, was vom Test erwartet bzw. wie dessen Verhalten (Behavior) ist. Allgemein ist die Syntax von Jasmine sehr einfach gehalten, sodass man einen Test lesen und verstehen kann, ohn...

Neugierig geworden?

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang