Thomas Bandt

Über mich | Kontakt | Archiv

JavaScript: Promises mit Mocha testen

Es sind die kleinen Dinge, die einem bei einer dynamischen Sprache wie JavaScript das Leben schwer machen können, wenn man nicht genau aufpasst.

Gestern habe ich für mein kleines Node-Demoprojekt begonnen, ein paar Tests zu schreiben. Die Wahl fiel hierbei auf Mocha als Testframework und zunächst Should, danach Chai für die “Assertions”. Chai deshalb, weil mir die Expect-Syntax besser gefällt, da sie auch mit Werten umgehen kann, die null oder undefined sind.

Jedenfalls habe ich mich auch dazu entschieden, asynchronen Code mit Promises (Q) zu schreiben und selbst wo möglich auf Callbacks zu verzichten.

Ein typisches System under Test sieht deshalb beispielsweise so aus:

(function(module) {
    "use strict";

    var Q = require("q");

    module.asyncFoo = function() {
        var deferred = Q.defer();

        setTimeout(function() {
            deferred.resolve("Hello World.");
        }, 100);

        return deferred.promise;
    }
}(module.exports));

Die Methode asyncFoo() liefert ein Promise zurück, das nach 100 Millisekunden mit dem Wert “Hello World.” aufgelöst wird.

Mein erster Test hierfür sah so aus:

(function() {
    "use strict";

    var expect = require('chai').expect;

    describe("Demo", function() {
        var demo;

        before(function() {
            demo = require("./demo");
        });

        it("returns Hello World", function() {
            demo.asyncFoo().then(function(result) {
                expect(result).to.equal("Hello World!");
            })
        });
    });
}());

Und hurra, er läuft durch und ist grün. Aber … man beachte das Satzzeichen. Eigentlich müsste er rot sein.

Mocha kann mit asynchronem Code umgehen, auch wenn es erst ma leine kleine Weile gebraucht hat, bis ich verstanden habe, dass der done()-Callback in die entsprechenden Methoden, in dem Fall it(), gesteckt werden muss … tut man das, wartet das Framework für jeden Test standardmäßig 2 Sekunden.

In diesen 2 Sekunden, die man auch hoch- oder runtersetzen kann, hat der Test Zeit, ausgeführt zu werden. Wichtig ist, dass am Ende der done()-Callback aufgerufen wird, damit das Framework weiß, dass die Ausführung beendet wurde.

Überschreitet man die Ausführungszeit oder ruft man den Callback nicht auf, gibt es glücklicherweise eine passende Exception, die einen darauf hinweist - das wird also nicht verschluckt.

So wird nun ein Schuh draus und der Test wird - korrekterweise - rot:

it("returns Hello World", function(done) {
    demo.asyncFoo().then(function(result) {
        expect(result).to.equal("Hello World!");
        done();
    })
});

Doch die Fehlermeldung passt nicht: Der Test wird deshalb rot, weil der Timeout erreicht ist.

Denn dadurch, dass Chai einen Fehler wirft, da die Erwartung nicht erfüllt ist (! statt .), wird der done()-Callback nicht aufgerufen. Das bedeutet, dass jeder Test, der fehlschlägt, auf diese Art genau 2 Sekunden läuft und keine passende Fehlermeldung geworfen wird.

Abhilfe kann hier schaffen, den done()-Callback in die Catch-Methode des Promises zu stecken:

it("returns Hello World", function(done) {
    demo.asyncFoo().then(function(result) {
        expect(result).to.equal("Hello World!");
        done();
    }).catch(done);
});

Sauberer wäre es zudem noch, statt then() done() zu verwenden, Zitat:

The Golden Rule of done vs. then usage is: either return your promise to someone else, or if the chain ends with you, call done to terminate it.

it("returns Hello World", function(done) {
    demo.asyncFoo().done(function(result) {
        expect(result).to.equal("Hello World!");
        done();
    }, done);
});

Wer kein Problem damit hat, noch eigene Erweiterungen einzubringen, die von den Standards der eingesetzten Frameworks abweichen, könnte den Aufruf auch noch rausziehen und so das Risiko minimieren, einmal done() zu vergessen.

function evaluateTestResult(promise, assert, done) {
    promise.done(
        function(result) {
            assert(result);
            done();
        },
        done
    );
}

it("returns Hello World", function(done) {
    evaluateTestResult(
        demo.asyncFoo(),
        function(result) {
            expect(result).to.equal("Hello World!");
        },
        done
    );
});

Das war zunächst intuitiv mein erster Schritt - nachdem es aber außer dem sicheren Aufruf von done() keinen weiteren Mehrwert bietet, werde ich erst mal die vorletzte Variante verwenden.



« Zurück  |  Weiter »