JavaScript Framework

Buster.JS – alles Wissenswerte zum All-in-one JavaScript-Testwerkzeug
Kommentare

In diesem Artikel wird ein genauer Blick auf das neue Framework Buster.JS geworfen, das alle Vorzüge bisheriger JS-Frameworks vereint. Es unterstützt das Testen mittels statischer HTML-Seiten wie QUnit, von asynchronem Code wie Mocha bis zu serverseitigem Code wie Nodeunit. Zudem kann Buster.JS wie JsTestDriver automatisiert gleichzeitig in verschiedenen Browsern testen. Das vereinfacht und beschleunigt das Ausführen der Tests deutlich. 

Der maßgebliche Initiator von Buster.JS (Infos hier bzw. hier) ist kein Unbekannter in der JavaScript-Szene. Christian Johansen verfasste bereits das Buch „Test-Driven JavaScript Development“. Der Norweger kreierte zudem das beliebte Stubbing- und Mocking-Framework Sinon.JS und das vielseitige JavaScript-Werkzeug Juicer. Buster.JS existiert derzeit zwar erst in der Betaversion, doch noch in diesem Jahr ist das Release für die stabile Version 1.0 geplant.

Erste Schritte mit Buster.JS

Wie Unit Tests für Buster.JS aussehen und wie sie im Browser und in Node.js ausgeführt werden, wollen wir uns anhand eines einfachen Beispiels anschauen. Dazu erstellen wir ein Modul room, das Teil einer Alarmanlage sein soll. Das Modul ermöglicht das Erzeugen von Objekten, die die zu überwachenden Räume repräsentieren. Ein Raum verwaltet eine Liste von Sensoren und kann diese asynchron auslesen.

Zunächst muss Buster.JS installiert und konfiguriert werden. Buster.JS ist ein Node.js-Modul, weswegen es sich einfach über den Node Packet Manager mittels npm install -g buster installieren lässt. Diese Installation funktioniert inzwischen auch unter Windows, auch wenn die Verwendung von Buster.JS dort manchmal noch ein wenig hakt. Eine vollständige Unterstützung für Windows ist erst für die Version 1.0 geplant.

Nach der Installation muss Buster.JS mitgeteilt werden, wo sich die Quell- und Testdateien befinden und in welcher Umgebung die Tests ausgeführt werden sollen: im Browser oder in Node.js? Die Konfiguration erfolgt in Form eines CommonJS-Moduls, also in JavaScript. Bei Buster.JS unterscheidet man zwischen einer Konfigurationsdatei und den eigentlichen Konfigurationen, Konfigurationsgruppen genannt. Eine Konfigurationsdatei kann dabei mehrere Konfigurationen enthalten. Das Modul exportiert deshalb ein Array von Konfigurationsobjekten. Wie sich später noch zeigen wird, können sie sich gegenseitig sogar erweitern.

Automatisiert im Browser testen

Die Konfiguration für das Testen im Browser ist in Listing 1 dargestellt. Alle Pfade in der Datei sind relativ und werden standardmäßig ausgehend vom Verzeichnis der Konfigurationsdatei aufgelöst. Es ist aber auch möglich, ein Verzeichnis anzugeben, von dem aus die Pfade aufgelöst werden sollen.

config["browser tests"] = {
  environment: "browser",
  libs: [
    "lib/**/*.js"
  ],
  sources: [
    "src/as.js"
    "src/**/*.js"
  ],
  tests: [
    "test/**/*-test.js"
  ]
};

Wir benennen die Konfigurationsdatei buster.js und speichern sie im Wurzelverzeichnis des Projekts oder im Unterverzeichnis /test/ oder /spec/, damit Buster.JS sie automatisch findet. Um die zu verwendende Konfigurationsdatei explizit anzugeben, kann alternativ auch das Kommandozeilenargument –config eingesetzt werden.

Vor allen anderen Quelldateien wird eine JavaScript-Datei namens as.js geladen. In dieser JavaScript-Datei wird der Namensraum „as“ (Alarm System) für die Anwendung erzeugt, über den dann auf die Module der Anwendung zugegriffen wird.

Entsprechend der Konfiguration werden alle Dateien, die sich unterhalb des Verzeichnis /test/ befinden und auf -test.js enden als Tests ausgeführt. Um das Modul room zu testen, erstellen wir die Datei room-test.js und legen sie direkt im Verzeichnis /test/ ab. In einem ersten Test wollen wir prüfen, ob das Hinzufügen eines Sensors zu einem Raum funktioniert (Listing 2).

var assert = buster.assert;

buster.testCase("room module", {

  setUp: function () {
    this.room = new as.Room();
  },

  "add a sensor to the room": function () {

    var sensor = {};
    this.room.addSensor(sensor);
    assert.equals(this.room.getSensors().length, 1);
  }
});

Die Syntax der Testdatei entspricht der von xUnit; das heißt, die Tests beziehungsweise Testmethoden sind in Testfällen zusammengefasst. Einem Testfall können die Methoden setUp und tearDown hinzugefügt werden, die vor beziehungsweise nach jeder Testmethode ausgeführt werden.

Darüber hinaus ist Buster.JS, ebenso wie JsTestDriver, in der Lage, Browser zu automatisieren. Das Besondere an der Browserautomatisierung ist, dass die Tests in beliebig vielen und insbesondere verschiedenen Browsern gleichzeitig ausgeführt werden können. Folgende Schritte sind dazu notwendig. Im ersten Schritt wird der Server gestartet:

buster @ubuntu:~$ buster-server
buster-server running on ht tp://localhost:1111

Wird kein Port angegeben, verwendet Buster.JS standardmäßig den Port 1111. Sobald der Server erfolgreich gestartet wurde, kann im zweiten Schritt der erste Browser geöffnet und in die Adresszeile der URL des Servers eingegeben werden. In unserem Beispiel ist das ht tp://localhost:1111. Daraufhin erscheint die Seite aus Abbildung 1. Auf dieser muss die Schaltfläche CAPTURE BROWSER betätigt werden. Anschließend kann der Browser minimiert und der Schritt für beliebig viele Browser wiederholt werden. Danach steht im dritten Schritt das Ausführen der Tests:

buster @ubuntu:~$ buster-test
Chrome 24.0.1312.56, Ubuntu Chromium: .
Firefox 19.0, Ubuntu:                 .
2 test cases, 2 tests, 2 assertions, 0 failures, 0 errors, 0 timeouts
Finished in 0.015s
buster @ubuntu:~$

Zunächst wird ein Client beim Ausführen der Tests gestartet. Dieser Client liest die Konfigurationsdatei ein, lädt die dort angegebenen Quell- und Testdateien und schickt sie zum Server. Der Server überträgt die Dateien zu den Browsern, wo die Tests im Anschluss ausgeführt werden. Den Status und das Ergebnis der Tests melden die Browser zurück an den Server und der Server zurück an den Client.

Nach Änderungen an der Implementierung braucht nur Schritt 3 wiederholt zu werden. Den Server und die Browser lässt man im Hintergrund weiterlaufen. Das vereinfacht die testgetriebene Entwickelung erheblich.

Abb. 1: Buster.JS in Aktion 

JAX Countdown
Der Countdown läuft: In großen Schritten nähern wir uns der JAX 2014, die in diesem Jahr vom 12. bis 16. Mai in Mainz stattfindet. Für alle Java-Entwickler, die sich auch für JavaScript interessieren, hat die JAX Sessions wie „Einführung in die Sprache JavaScript für Java-Entwickler“ mit Oliver Zeigermann oder „TypeScript: JavaScript für Java-Entwickler?“ mit Kai Tödter im Programm.
Bis zum 10. April profitieren Sie noch von den Frühbucherpreisen – übrigens auch bei den parallel stattfindenden BigDataCon und Business Technology Days!
Alle Infos unter www.jax.de.

 

AufmacherbildRed mouse multifunction swiss knife metaphor von istockphoto / Urheberrecht: LUNAMARINA

 

[ header = Seite 2: Testen von serverseitigem JavaScript in Node.js]

 

Testen von serverseitigem JavaScript in Node.js

Da wir Buster.JS durch den Schalter -g global installiert haben, müssen wir für unser Projekt einen Link darauf anlegen. Das erreichen wir durch den Aufruf von npm link buster im Wurzelverzeichnis des Projekts.

Damit unser Test nun auch für Node.js ausgeführt wird, müssen wir eine zusätzliche Konfiguration erstellen. Dazu erweitern wir die vorhandene Konfigurationsdatei um folgenden Code:

config["node.js tests"] = {
  extends: "browser tests",
  environment: "node"
};

Aufgrund des Modulsystems von Node.js muss der Test erweitert werden: erstens um eine Anweisung zum Laden des Moduls Buster.JS und zweitens um eine Anweisung zum Laden des Moduls für den Namensraum der Anwendung. Durch eine zusätzliche Bedingung wird sichergestellt, dass die Anweisungen nur für Node.js ausgeführt werden:

if (typeof require === "function" && typeof module === "object") {
  var buster = require("buster");
  var as = require("../src/as");
}

Auch an den Quelldateien müssen ähnliche Anpassungen vorgenommen werden. Anschließend können die Tests wieder mit dem Kommando buster-test ausgeführt werden (Listing 3).

buster @ubuntu:~$ buster-test
Chrome 24.0.1312.56, Ubuntu Chromium: .
Firefox 19.0, Ubuntu:                 .
2 test cases, 2 tests, 2 assertions, 0 failures, 0 errors, 0 timeouts
Finished in 0.016s
room module: .
1 test case, 1 test, 1 assertion, 0 failures, 0 errors, 0 timeouts
Finished in 0.006s

Asynchrone Funktionalität testen

Das Testen asynchroner Funktionalität zählt zu den elementaren Anforderungen an ein Testwerkzeug für JavaScript. Buster.JS bietet dafür drei Möglichkeiten an:

Erstens: Buster.JS enthält das Stubbing- und Mocking-Framework Sinon.JS. Sinon.JS stellt das clock-Objekt zur Manipulation der Zeit bereit. Darüber kann zum Beispiel getestet werden, ob Funktionen, die mittels setTimeout asynchron ausgeführt werden sollen, auch tatsächlich ausgeführt wurden. Da es sich dabei um eine reine Sinon.JS-Funktionalität handelt und Sinon.JS sehr einfach in jedes Testwerkzeug eingebunden werden kann, wird diese Möglichkeit hier nicht genauer betrachtet.

Zweitens: Durch Hinzufügen des Parameters done zur Signatur einer Testmethode wird aus einem synchronen ein asynchroner Test (Listing 4).

"read sensors": function (done) {

  ...

  room.readSensors(done(function (err, results) {
    assert.equals(room.getSensors()[0].state, 0);
    assert.equals(room.getSensors()[1].state, 1);
  }));
}

Der Parameter done markiert den Test als asynchron. Bei der Ausführung des Tests wird für den Parameter ein Callback übergeben. Das Callback muss innerhalb des Tests aufgerufen werden, um das Ende des Tests zu markieren. Wird es nicht oder nicht rechtzeitig aufgerufen, läuft der Test in einen Timeout und schlägt letztendlich fehl.

Drittens: Alternativ zum Parameter done kann der Test auch ein Promise zurückliefern. Buster.JS wertet jedes Objekt als Promise, das eine Methode then besitzt. Unter Verwendung von when.js könnte der Test wie in Listing 5 aussehen.

"read sensors": function () {

  var deferred = when.defer();

  room.readSensors(function (err, results) {
    assert.equals(room.getSensors()[0].state, 0);
    assert.equals(room.getSensors()[1].state, 1);
    deferred.resolver.resolve();
  });

  return deferred.promise;
} 

Assertions

Anders als viele andere Unit-Test-Werkzeuge, verwendet Buster.JS nicht das Präfix not, um eine Assertion zu negieren, sondern besitzt dafür ein passendes Gegenstück: die Refutation. Statt assert.notEquals(foo, bar) wird in Buster.JS refute.equals(foo, bar) eingesetzt.

Buster.JS besitzt eine Menge vordefinierter Assertions. Es erlaubt aber auch das Hinzufügen eigener Assertions. Beim Testen der Alarmanlagensoftware ist es sicherlich hin und wieder notwendig zu prüfen, ob es in einem Raum einen Alarm gibt oder nicht. Sofern die Anwendung selbst keine Funktion bereitstellt, mit der dies abgefragt werden kann, könnte man sich für den Test eine entsprechende Helferfunktion schreiben. Die Verwendung einer eigenen Assertion ist allerdings noch eleganter (Listing 6). Sie ist komfortabel und integriert. Sie wird somit auch automatisch im Reporting mit eingebunden.

buster.assertions.add("isSafe", {
  assert: function (room) {

    for (var i = 0; i < room.sensors.length; i++) {
      if (room.sensors[i].state !== 1) {
        return false;
      }
    }

    return true;
  },
  assertMessage: "Expected the room ${0} to be safe!",
  refuteMessage: "Expected the room ${0} to be not safe!",
  expectation: "toBeSafe"
});

Verwendet werden kann die Assertion dann durch assert.isSafe(room) und refute.isSafe(room).

Auszuführende Tests einschränken

Es gibt Situationen, in denen man nicht alle Tests ausführen will, sondern nur eine Teilmenge, beispielsweise um nach der Ursache für einen Fehler zu suchen. Buster.JS bietet verschiedene Filter an, um die auszuführenden Tests zu beschränken. Mit der Option –environment oder -e kann die Testausführung auf Browser (browser) oder auf Node.js (node) beschränkt werden. Mit der Option –config-group oder -g kann eine Liste von Konfigurationsgruppen angegeben werden, deren Tests ausgeführt werden sollen. Ein Komma trennt die einzelnen Elemente. Buster.JS interpretiert dabei die Angabe eines Elements als regulären Ausdruck. Mit einer Angabe können also auch die Tests mehrerer Konfigurationsgruppen ausgeführt werden, sofern deren Name zum regulären Ausdruck passt.

Ohne Angabe einer Option kann eine Liste mit den Namen von Tests oder Testfällen angegeben werden, die ausgeführt werden sollen. Auch hier müssen die einzelnen Elemente durch ein Komma getrennt werden und auch hier wird jedes Element als regulärer Ausdruck interpretiert. Da all diese Angaben als Filter wirken, können sie beliebig kombiniert werden.

Um genau einen Test auszuführen, beispielsweise zum Debuggen, kann man auch die so genannte Fokusrakete verwenden. Dabei handelt es sich um die Zeichenfolge „=>“, die dem Namen des Tests vorangestellt wird, der ausgeführt werden soll:

"=>read sensors" : function (done) {
  ...
} 

Tests temporär deaktivieren

Manchmal möchte man als Entwickler einen Test temporär deaktivieren, zum Beispiel wenn ein Test für eine Funktionalität bereits geschrieben worden ist, diese aber erst in naher Zukunft implementiert wird. Auch das ist mit Buster.JS möglich. Durch Voranstellen von „//“ zum Namen eines Tests oder Testfalles, wird dieser nicht ausgeführt:

"//add a sensor to the room": function () {
  ...
}

Als Erinnerung gibt Buster.JS eine Liste aller ausgesetzten Tests im Report aus. So können diese nicht in Vergessenheit geraten (Listing 7).

buster @ubuntu:~$ buster-test
Chrome 24.0.1312.56, Ubuntu Chromium: ..
Firefox 19.0, Ubuntu:                 ..

Deferred: Chrome 24.0.1312.56, Ubuntu Chromium room module add a sensor to the room
Deferred: Firefox 19.0, Ubuntu room module add a sensor to the room

2 test cases, 4 tests, 12 assertions, 0 failures, 0 errors, 0 timeouts, 2 deferred
Finished in 0.441s
room module: ..

Deferred: room module add a sensor to the room

1 test case, 2 tests, 6 assertions, 0 failures, 0 errors, 0 timeouts, 1 deferred
Finished in 0.444s

[ header = Seite 3: Mehr Struktur durch geschachtelte Testfälle]

Mehr Struktur durch geschachtelte Testfälle

Die Menge der Tests für unser Beispiel hält sich noch im Rahmen. Mit steigender Anzahl wird es allerdings immer schwieriger, den Überblick zu behalten. Außerdem bekommt man häufig das Problem, dass man für eine gewisse Teilmenge an Tests ein gemeinsames Set-up benötigt, aber eben nicht für alle Tests eines Testfalls. Buster.JS bietet ein einfaches aber wirksames Instrument an, um mehr Struktur in die Tests zu bekommen: geschachtelte Testfälle. Jeder Testfall kann beliebig viele andere Testfälle enthalten und die Schachtelung kann beliebig tief erfolgen. Auf diese Weise lassen sich Tests einfach gruppieren. Vor der Ausführung eines Tests werden alle setUp-Methoden der umgebenden Testfälle von außen nach innen aufgerufen und abschließend alle tearDown-Methoden von innen nach außen.

So ließen sich beispielsweise die beiden Varianten zum Testen von asynchroner Funktionalität aus unserem Beispiel wie folgt in einen geschachtelten Testfall gruppieren (Listing 8).

buster.testCase("room module", {

  ...

  "read sensors with": {
    "done callback": function (done) {
      ...
    },

    "promise": function () {
      ...
    }
  }
});

Im Fehlerfall fügt Buster.JS die Namen der geschachtelten Testfälle und den Namen des fehlgeschlagenen Tests zusammen.

Feature Detection

Mit dem Objekt requiresSupportFor können für einen Testfall Bedingungen angegeben werden, die erfüllt sein müssen, damit der Testfall ausgeführt wird. Das können grundsätzlich beliebige Bedingungen sein. Gedacht ist diese Funktion aber dafür, dass Tests nur dann ausgeführt werden, wenn die dafür notwendigen Funktionen von der Laufzeitumgebung unterstützt werden. Auf diese Weise können wir zum Beispiel Tests schreiben, die sicherstellen, dass sich die Anwendung bei auftretenden Touch-Events korrekt verhält und durch Angabe einer Bedingung dafür sorgen, dass die Tests nur für Touchscreen-Geräte ausgeführt werden (Listing 9).

buster.testCase("touch event tests", {
  requiresSupportFor: {
    "touch events": !!('ontouchstart' in window) || !!('onmsgesturechange' in window)
  },

  "should receive touch events": function () {
    ...
  },

  ...
});

Mittels der Bedingung touch events prüfen wir, ob ein Touchscreen-Gerät vorliegt oder nicht. Auf einem Gerät ohne Touchscreen werden wir bei Ausführung der Tests darüber informiert, dass die Tests übersprungen wurden:

...
Skipping touch event tests, unsupported requirement: touch events
Skipping touch event tests, unsupported requirement: touch events

4 test cases, 6 tests, 14 assertions, 0 failures, 0 errors, 0 timeouts
Finished in 0.437s

BDD mit Buster.JS

Buster.JS unterstützt neben der xUnit-Syntax auch die BDD-Syntax inklusive Expectations und bietet somit auch eine Alternative für Entwickler, die bislang beispielsweise Jasmine verwenden. Auch für die Tests in BDD-Syntax bietet es sich an, lokale Variablen für die verwendeten Funktionen wie zum Beispiel buster.spec.describe anzulegen (Listing 10).

var describe = buster.spec.describe;
var before = buster.spec.before;
var it = buster.spec.it;
var expect = buster.assertions.expect;

var spec = describe("room module", function () {
  before(function () {
    this.room = new as.Room();
  });

  it("should add a sensor to the room", function () {
    var sensor = {};
    this.room.addSensor(sensor);
    expect(this.room.getSensors().length).toEqual(1);
  });
});

Wem es zu aufwändig ist, Variablen für die Funktionen der BDD-Syntax anzulegen, kann stattdessen auch durch den Aufruf von buster.spec.expose() die Funktionen global machen.

Für jede Assertion in Buster.JS gibt es eine entsprechende Expectation. Die Expectation für die Assertion isSafe, die wir zuvor im Abschnitt Assertions erstellt haben, ist toBeSafe. Mittels dieser kann ebenfalls festgestellt werden, ob ein Raum sicher expect(this.room).toBeSafe(); beziehungsweise nicht sicher ist expect(this.room).not.toBeSafe();.

Was gibt es noch?

Die Liste der Funktionen, die Buster.JS bereitstellt, ist zu lang, um hier alle detailliert zu beschreiben. Die folgende Liste gibt einen Überblick weiterer nützlicher Funktionen mit einer kurzen Beschreibung:

  • Testen mit statischen HTML-Seiten: Wie die meisten Unit-Test-Werkzeuge für JavaScript unterstützt auch Buster.JS das Testen mittels statischer HTML-Seiten im Stil von QUnit.
  • Integriertes Sinon.JS: Wie im Abschnitt „Asynchrone Funktionalität testen“ erwähnt, ist Sinon.JS, ein Framework zum Erzeugen von Spies, Stubs und Mocks, bereits in Buster.JS integriert. Somit kann man von Anfang an gut isolierte Unit-Tests schreiben.
  • Reporter: Buster.JS enthält verschiedene Reporter, das heißt es kann den Verlauf und das Ergebnis eines Testlaufs in verschiedenen Formaten bereitstellen: XML (xUnit), dots (Matrix aus Punkten für grüne Tests, „F“ für fehlerhafte usw.), Specification (nodeunit), TAP (Test Anything Protocol), TeamCity und mehr.
  • Buster AMD: Erweiterung zum Testen von Anwendungen, die AMD (Asynchronous Module Definition) und einen Programmlader nutzen.
  • Benutzerdefinierte Testumgebung: Es kann eine HTLM-Datei angegeben werden, in der die Tests ausgeführt werden. Damit lassen sich die Tests beispielsweise in einem HTML4-Strict-Dokument ausführen statt in einem HTML5-Dokument, was der Standard bei Buster.JS ist. Diese Funktion ist allerdings noch nicht in der Betaversion implementiert.
  • Headless Browser Testing: Wenn man den Buster.JS-Server nicht startet, werden die Browsertests headless in PhantomJS ausgeführt. Auch diese Funktion ist noch nicht in der Betaversion implementiert.

Fazit

Buster.JS deckt aufgrund seiner zahlreichen Funktionen fast alle Aspekte des Unit-Testens von JavaScript-Anwendungen ab. Benötigt der Entwickler dennoch weitere Funktionalitäten oder entspricht eine vorhandene Funktionalität nicht ganz den Bedürfnissen, kann Buster.JS sehr einfach angepasst werden. So können für weitere Ausgabeformate Reporter ergänzt und Wrapper erstellt werden, um Tests anderer Test-Frameworks in Buster.JS auszuführen, weitere Testsyntaxen aufgenommen werden und vieles mehr. Ausprobieren lohnt sich.

Zusatzinfo:
In Ausgabe 3.13 des PHP Magazins wurden verschiedene Frameworks für das Testen in JavaScript vorgestellt. Hier geht’s zum Artikel „JavaScript client- und serverseitig testen„.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -