Andreas Rozek
[ Impressum ]   [ Datenschutzerklärung ]   [ Kontakt ]       deutsche Fassung   [ english version ]

Jasmine

Jasmine [1] ist eine gut gemachte Bibliothek für das "Behavior-driven" Testen von Javascript-Funktionen. Für Jasmine erstellte Tests können sowohl im Backend (unter Node.js [2]) als auch im Frontend (d.h. im Browser) laufen.

Auf dieser Seite erfahren Sie

  • wie sich asynchron geworfene Ausnahmen überprüfen und
  • mehrere Testdurchläufe im Browser erreichen lassen und
  • wie man einen angepassten "Reporter" erstellt.

Eine Anleitung erklärt Ihnen die Bedienung der Seiten in diesem Web-Auftritt, in den Einstellungen können Sie deren Verhalten anpassen.

Prüfen asynchron geworfener Ausnahmen

Jasmine kann (synchron) geworfene Ausnahmen überprüfen und auf Ergebnisse asynchroner Funktionen warten - das Prüfen asynchron geworfener Ausnahmen wird jedoch nicht direkt unterstützt. Glücklicherweise ist nur wenig Aufwand nötig, um diesen Mangel zu beheben.

Das Prüfen synchron geworfener Ausnahmen ist einfach:

describe('test of synchronously thrown exceptions', function() {
it('synchronously thrown exceptions are simple', async function() {
function erroneousFunction () {
throw new Error('function failed')
}

expect(erroneousFunction).toThrow()
expect(erroneousFunction).toThrow(new Error('function failed'))
expect(erroneousFunction).toThrowError('function failed')
expect(erroneousFunction).toThrowError(Error,'function failed')
})
})

Auch das Warten auf Ergebnisse asynchroner Funktionen ist nicht schwierig:

describe('test of asynchronously delivered results', async function() {
it('asynchronously delivered function results are simple', async function() {
function asyncResult () {
return new Promise((resolve,reject) => {
setTimeout(() => resolve('delivered'), 10)
})
}

expect(await asyncResult()).toBe('delivered')
})
})

Die (eigentlich naheliegende) Kombination beider Verfahren schlägt jedoch fehl:

describe('test of asynchronously thrown exceptions', async function() {
it('combining both approaches will fail', async function() {
function erroneousFunction () {
return new Promise((resolve,reject) => {
setTimeout(() => reject(new Error('function failed')), 10)
})
}

expect(await erroneousFunction).toThrow()
expect(await erroneousFunction).toThrow(new Error('function failed'))
expect(await erroneousFunction).toThrowError('function failed')
expect(await erroneousFunction).toThrowError(Error,'function failed')
})
})

Abhilfe bieten ein bis zwei kleine Hilfsfunktionen, die geworfene Ausnahmen abfangen und als "normale" Rückgabewerte an den Test ausliefern:

describe('test of asynchronously thrown exceptions, done right', async function() {
it('a slightly different approach will succeed', async function() {
function erroneousFunction () {
return new Promise((resolve,reject) => {
setTimeout(() => reject(new Error('function failed')), 10)
})
}

function thrownMessageOf (FunctionToTest) {
return FunctionToTest().then(
() => undefined,
(Signal) => Signal.message || Signal.toString()
)
}

function thrownExceptionOf (FunctionToTest) {
return FunctionToTest().then(
() => undefined,
(Signal) => Signal
)
}

expect(await thrownMessageOf(erroneousFunction)).toBeDefined()
expect(await thrownMessageOf(erroneousFunction)).toBe('function failed')
expect(await thrownExceptionOf(erroneousFunction)).toBeDefined()
expect(await thrownExceptionOf(erroneousFunction)).toEqual(new Error('function failed'))
})
})

Sie sollten also vorab folgende beiden Funktionen definieren:

function thrownMessageOf (FunctionToTest) {
return FunctionToTest().then(
() => undefined,
(Signal) => Signal.message || Signal.toString()
)
}

function thrownExceptionOf (FunctionToTest) {
return FunctionToTest().then(
() => undefined,
(Signal) => Signal
)
}

Und den eigentlichen Test wie folgt formulieren:

expect(await thrownMessageOf(async throwing Function)).toBeDefined()
expect(await thrownMessageOf(async throwing Function)).toBe(your error message)

expect(await thrownExceptionOf(async throwing Function)).toBeDefined()
expect(await thrownExceptionOf(async throwing Function)).toEqual(your thrown object)

Mehrere Testdurchläufe im Browser

Jasmine kann Tests direkt im Browser ausführen und protokollieren. Leider werden die Tests genau einmal, und zwar unmittelbar nach dem Besuchen der zugehörigen Web-Seite ausgeführt. Mit nur wenig Aufwand kann man Jasmine jedoch dazu bringen, die Tests gezielt auf Anforderung hin auszuführen

Der Trick besteht darin, die "Umgebung", die sich Jasmine nach dem Laden der benötigten CSS- und JavaScript-Dateien aufbaut, (noch vor dem Anlegen der eigentlichen Tests) "zurückzusetzen" und die Tests später explizit zu starten.

Dazu benötigen Sie die folgenden zwei kleinen Funktionen

function Jasmine_reset () {
window['jasmine'] = jasmineRequire.core(jasmineRequire);

var jasmineEnvironment = jasmine.getEnv(); // Jasmine environment
var jasmineInterface = jasmineRequire.interface(jasmine, jasmineEnvironment);
function extend (Target, Source) {
for (var Key in Source) { Target[Key] = Source[Key] };
return Target;
};
extend(window, jasmineInterface);
};

function Jasmine_runTests () {
window['jasmine'].getEnv().execute();
};

die Sie am besten unmittelbar nach dem Laden der zu Jasmine gehörenden Skript-Dateien definieren.

Vor dem Anlegen der eigentlichen Tests setzen Sie zunächst die Testumgebung von Jasmine zurück:

Jasmine_reset();

Nach dem Anlegen der Tests können Sie diese nun bequem mittels

Jasmine_runTests();

starten.

Die Abfolge

Jasmine_reset();
define your tests
Jasmine_runTests();

kann beliebig wiederholt werden - sowohl mit denselben als auch mit unterschiedlichen Tests.

Anwendungsspezifischer "Reporter"

Die Form der Protokollierung von im Browser durchgeführten Tests ist eigentlich durch Jasmine festgelegt. Glücklicherweise lässt sich diese Vorgabe jedoch umgehen und die Ausgabe z.B. an die Erfordernisse einer Browser-gestützten Entwicklungsumgebung anpassen.

Der Trick besteht darin, in die Testumgebung von Jasmine einen speziellen "Reporter" einzutragen, der anschließend während der Tests die Protokollierung übernimmt.

Falls Sie die zuvor gezeigte Funktion Jasmine_reset verwenden, können Sie diese einfach entsprechend erweitern:

function Jasmine_reset () {
window['jasmine'] = jasmineRequire.core(jasmineRequire);

var jasmineEnvironment = jasmine.getEnv(); // Jasmine environment
var jasmineInterface = jasmineRequire.interface(jasmine, jasmineEnvironment);
function extend (Target, Source) {
for (var Key in Source) { Target[Key] = Source[Key] };
return Target;
};
extend(window, jasmineInterface);

var customReporter = {
your custom reporter, see below
};
jasmineEnvironment.addReporter(customReporter);
};

Ein "Reporter" ist nichts anderes als ein einfaches JavaScript-Objekt mit ein paar spezifischen Methoden:

var customReporter = {
jasmineStarted: function (SuiteInfo) { ... },
suiteStarted: function (Result) { ... },
specStarted: function (Result) { ... },
specDone: function (Result) { ... },
suiteDone: function (Result) { ... },
jasmineDone: function (Result) { ... }
}

Einen eigenen "Reporter" zu schreiben, ist nicht sonderlich schwierig - nähere Hinweise dazu (und ein einfaches Beispiel) finden Sie in der Dokumentation zu Jasmine.

Es müssen nicht alle zuvor erwähnten Methoden implementiert werden. Zweckmäßigerweise beginnen Sie mit dem Beispiel aus der Dokumentation und passen die Ausgabe schrittweise an Ihre Bedürfnisse an.

Falls Sie drei kleine Funktionen definieren, die die eigentliche Ausgabe steuern

function   clear () { insert your own implementation }
function print () { insert your own implementation }
function println () { insert your own implementation }

können Sie mit folgender Implementierung für einen einfachen "Reporter" beginnen:

var customReporter = {
jasmineStarted: function (SuiteInfo) {
clear();
println('Running suite with ' + SuiteInfo.totalSpecsDefined + ' Specs');
},
suiteStarted: function (Result) {
println('"' + Result.description + '":');
},
specStarted: function (Result) {
print(' - "' + Result.description + '":');
},
specDone: function (Result) {
println(' ' + Result.status);
for (var i = 0, l = Result.failedExpectations.length; i < l; i++) {
var Failure = Result.failedExpectations[i];
println(' Failure: ' + Failure.message);
println(Failure.stack);
};
},
jasmineDone: function (Result) {
println('all tests completed');
for (var i = 0, l = Result.failedExpectations.length; i < l; i++) {
var Failure = Result.failedExpectations[i];
println('Global ' + Failure.message);
println(Failure.stack);
};
},
};
jasmineEnvironment.addReporter(customReporter);

Literaturhinweise

[1] Pivotal Labs
Jasmine - Behavior-Driven JavaScript
(siehe https://jasmine.github.io/)
MIT-Lizenz
Jasmine ist eine gut gemachte Bibliothek für das "Behavior-driven" Testen von Javascript-Funktionen. Für Jasmine erstellte Tests können sowohl im Backend (unter Node.js) als auch im Frontend (d.h. im Browser) laufen
[2] OpenJS Foundation
Node.js
(siehe https://nodejs.org/)
verschiedene Lizenzen
aus Wikipedia: "Node.js ist eine plattformübergreifende Open-Source-JavaScript-Laufzeitumgebung, die JavaScript-Code außerhalb eines Webbrowsers ausführen kann"

Diese Web-Seite verwendet die folgenden Drittanbieter-Bibliotheken oder -Materialien bzw. StackOverflow-Antworten:

Der Autor dankt den Entwicklern und Autoren der genannten Beiträge für ihre Mühe und die Bereitschaft, ihre Werke der Allgemeinheit zur Verfügung zu stellen.