Unit-Tests mit dem JavaScript-Framework Jasmine

Seite 2: Auftakt

Inhaltsverzeichnis

In Jasmine beginnt eine Test-Suite mit dem Aufruf einer globalen Jasmine-Funktion und zwei Parametern. Spezifikationen definiert die globale Jasmine-Funktion it, die als Parameter Titel und Funktion zugewiesen bekommt. Die Funktion kann eine Spezifikation oder ein Test sein. Jede Spezifikation enthält eine oder mehrere Erwartungen, die bei Überprüfung entweder wahr oder falsch sein können. Nur wenn sich alle definierten Erwartungen bewahrheiten, ist eine Spezifikation erfüllt, also bestanden.

it 'Addition zweier positiver Zahlen', ->

Die eigentlichen Testfälle fasst man in sogenannten Suites zusammen. Gruppiert werden sie innerhalb von describe.

describe 'Calc', ->

Im Beispiel ist Calc der Name der Test-Suite. Innerhalb der einzelnen Tests befinden sich die jeweiligen Beschreibungen.

it 'Addition zweier positiver Zahlen', ->
it 'Subtraktion zweier positiver Zahlen', ->

Um Tests korrekt zu initialisieren, lässt sich innerhalb einer Test-Suite über beforeEach eine Funktion ausführen. Das Gegenstück dazu ist die Spec afterEach, die sich nach jedem Test verwenden lässt. Dabei übergibt man ihr die nach dem Test gestartete Funktion.

Damit sich Code überhaupt einem Unit-Test unterziehen lässt, muss er dafür geeignet sein. Man kann nicht jeden beliebigen JavaScript-Code in Jasmine (oder einem anderen Unit-Test-Framework) testen. Entscheidend ist die strikte Trennung der Code-Elemente. Üblicherweise sollte der zu überprüfende JavaScript-Code in einer externen js-Datei liegen. Die HTML- und CSS-Syntax steht dann in einer separaten Datei. So lässt sich der JavaScript-Code unabhängig von den anderen Komponenten testen.

Wichtig für einen Unit-Test ist deren Fähigkeit, möglichst unabhängig zu laufen. Allerdings lässt sich das in der Praxis nicht immer umsetzen. In solchen Fällen kommen "Spies" (Spione) ins Spiel. Hierüber lassen sich Funktionsaufrufe überprüfen. Jasmine zeichnet diese auf und stellt sie für spätere Prüfungen zur Verfügung. Derzeit sind hauptsächlich die sogenannten Test-Stubs möglich, für die Jasmine spezielle Matcher bereitstellt. So ist die Methode toHaveBeenCalled wahr, wenn Spy aufgerufen wird. Ein typisches Einsatzgebiet für sie ist das Überprüfen, ob eine Funktion tatsächlich aufgerufen wurde. In dem Zusammenhang ist auch der Matcher toHaveBeenCalledWith zu sehen. Er ist wahr, wenn die Liste der Argumente mit den aufgezeichneten Spy-Aufrufen übereinstimmt. Ebenso lassen sich aber auch ganze Funktionen durch Fälschungen ersetzen.

Kernstück der Tests bilden die sogenannten Matcher. Wer Tests in anderen Sprachen wie Java durchgeführt hat, dem ist sicherlich der Begriff Assertions (Zusicherungen) vertraut. Matchers bieten die Möglichkeit zu überprüfen, ob das Testergebnis richtig oder falsch ist. Den Test leitet immer ein expect-Aufruf ein.

it('neues element', function() {
this.stack.new('em');
expect(this.stack.neuElement()).leeresElement();
});

Man übergibt expect als Parameter den zu überprüfenden Ausdruck. Dabei kann es sich um eine Funktion, eine Variable oder um eine Objekteigenschaft handeln. Übergibt man eine Variable oder eine Objekteigenschaft, überprüft Jasmine diese direkt. Bei einer Funktion hingegen testet das Framework die Rückgabewerte. Im einfachsten Fall überprüft msn, ob der expect übergebene Wert mit seinem Parameter übereinstimmt. Man verwendet dafür den Matcher toEqual.

it("Berechnung anhalten", function() {
expect(stop.calc()).toEqual(stop);
});

Matcher sind nichts anderes als boolsche Vergleiche zwischen aktuellen und erwarteten Werten. Jasmine bekommt die Rückmeldung, ob Erwartungen erfüllt wurden oder nicht. In Jasmine stehen einige vordefinierte Matcher zur Verfügung. So kann man mit toThrow überprüfen, ob eine Funktion eine Fehlermeldung ausgibt. Aber auch toBeTruthy (Vergleich mit boolscher Wahrscheinlichkeit), toBeCloseTo (Präzision des mathematischen Vergleichs) und toMatch (für reguläre Ausdrücke) kommen oft zum Einsatz. Die Standard-Matcher decken also eine große Palette an Einsatzszenarien ab. Es besteht allerdings auch die Möglichkeit zur Definition eigener Matcher. Jeder Matcher lässt sich außerdem negieren, indem bei expect dem jeweiligen Matcher ein not vorangestellt wird.

Variablen können in JavaScript zu unterschiedlichen Zeitpunkten verschiedene Werte haben. Vor allem im Zusammenhang mit Ajax-Anwendungen ist das Phänomen vermehrt anzutreffen. Um das Verhalten simulieren und daraus resultierende Fehler abfangen zu können, lassen sich asynchrone Tests durchführen. Jasmine bietet mit den Funktionen runs, waits und waitFor einen Weg zur Simulation asynchronen Verhaltens. Dafür lassen sich Blöcke mit einer Reihe runs-Aufrufe definieren. Die so gekennzeichneten Blöcke arbeitet Jasmine sequenziell ab. runs übergibt man als Parameter die Funktion. Sie wird tatsächlich erst nach Ablauf der angegebenen Zeitspanne ausgeführt. Über waits lässt sich die Anzahl der Millisekunden spezifizieren, die vergehen sollen, bis der nächste runs-Block ausgeführt wird.

...
runs(function () {
expect(this.foo).toEqual(0);
});
waits(100);
runs(function () {
expect(this.foo).toEqual(1);
});
...

Neben runs und waits gibt es waitsFor. Als Parameter werden diesem Aufruf die Funktion, eine optionale Nachricht und der gewünschte Timeout-Wert übergeben. waitsFor wird solange ausgeführt, bis entweder der Timeout-Wert erreicht oder true zurückgeliefert wird.

Für einen Test im Browser braucht es nicht unbedingt Jasmine. Dazu hätte man durchaus den Vorgänger JsUnit verwenden oder weiterentwickeln können. Ein Vorteil von Jasmine ist aber die Möglichkeit, das Framework in eine IDE aufzunehmen. Schließlich muss man nicht erst die angestammte Arbeitsumgebung verlassen, um Tests durchzuführen. Denn Tests werden nur effektiv sein und vor allem regelmäßig durchgeführt, wenn sie für den Entwickler keinen großen Mehraufwand bedeuten.

Die notwendigen Dateien werden spezifiziert (Abb. 2)

Vermeiden lässt sich ein solcher Aufwand durch die direkte Integration des Test-Frameworks in die eigene Entwicklungsumgebung. Hierfür eignet sich JsTestDriver, ein JavaScript-Test-Runner, der eine automatisierte parallele Testausführung unterstützt. Um Unit-Tests in JavaScript überhaupt ausführen zu können, benötigt der Entwickler eine JavaScript-Engine. Bei JsTestDriver lassen sich mehrere Browser registrieren, auf denen die Tests laufen. Der Test-Runner unterstützt derzeit Chrome, Firefox, Opera, Internet Explorer und Safari.

Vor dem Hintergrund, dass eines der Hauptprobleme bei der Entwicklung von JavaScript-Anwendungen die unterschiedlichen Browser und Betriebssysteme sind, ist das ideal. Schließlich müsste man sonst als Entwickler die Anwendung auf eben jenen Zielplattformen testen, was leider nicht immer machbar ist. Entdeckt man nun einen Fehler im IE9 unter Windows und beseitigt ihn, heißt das noch lange nicht, dass der Code fehlerfrei in Chrome läuft. Code muss man in allen Browsern testen, die für das Projekt relevant erscheinen. Über die Browser-Symbole in der Werkzeugleiste startet der Entwickler den lokalen Browser und registriert ihn am JsTestDriver-Server. Anschließend kann er die Tests ausführen.

Der Test wird ausgeführt (Abb. 3).

Entwickler können ihre Anwendung somit parallel in unterschiedlichen Browsern testen. Sie können über Adapter JsTestDriver auch in Kombination mit Jasmine verwenden. Plug-ins gibt es für die IDEs WebStorm und Eclipse. Nach deren Installation sollten im zu testenden Projekt die Quelldateien für Jasmine und der Jasmine-Adapter liegen. Zusätzlich ist eine *.jstd-Datei anzulegen, in der die für den Test benötigten Dateien spezifiziert sind.

load:
-lib/jasmine-1.2.0/jasmine.js
-lib/jasmine-jstd-adapter/src/JasmineAdapter.js
-spec/calcspec.js
-src/calc.js

Über das in der IDE nach erfolgreicher Plug-in-Installation hinzugekommene Werkzeugfenster JsTestDriver Server lässt sich der lokale JsTestDriver-Server starten.