Warum umständlich, wenn …: Node.js-Server testen

the next big thing  –  0 Kommentare

Im Beitrag "Unabhängige Unit-Tests: HTTP-Anfragen abstrahieren" wurde das Modul nock vorgestellt, um auf komfortable Weise Code testen zu können, der HTTP-Anfragen stellt: Zu diesem Zweck fängt nock alle ausgehenden HTTP-Anfragen ab und gibt jeweils eine zuvor festgelegte Antwort zurück. Das ist für das Testen von Webclients ausgesprochen hilfreich.

Doch wie lässt sich der Webserver testen, der das andere Ende der Verbindung darstellt – nach Möglichkeit auf eine ähnlich einfache Art? Wie steht es um Anwendungen und Middleware, die auf Connect und Express basieren?

Selbstverständlich besteht stets die Möglichkeit, einen Webserver vor dem Ausführen der Unit-Tests zu starten, doch stößt man rasch an die Grenzen dieses Vorgehens: Da eine HTTP-Anfrage potenziell den Zustand des Webservers verändert, müsste man diesen beenden und erneut starten – was in Node.js serienmäßig nicht ohne Weiteres möglich ist.

SuperTest integrieren

Abhilfe schafft das Modul SuperTest von TJ Holowaychuk, der unter anderem auch Express und Mocha entwickelt. Die Installation erfolgt auf dem für Node.js üblichen Weg in den lokalen Kontext der Anwendung:

$ npm install supertest

Danach lässt sich SuperTest mit Hilfe der require-Funktion einbinden. TJ Holowaychuk empfiehlt in der Dokumentation von SuperTest, der verwendeten Variable den Namen request zuzuweisen:

var request = require('supertest');

Die Variable request enthält anschließend eine Funktion, die einen Webserver beziehungsweise eine Funktion der Form

function (req, res) {
// ...
}

als Parameter akzeptiert. Daher kann man request beispielsweise eine auf Basis von Express entwickelte Anwendung übergeben – oder eine neu erzeugte Anwendung, der man zuvor eine Middleware hinzugefügt hat:

var express = require('express'),
request = require('supertest');

var app = express();
app.use(function (req, res, next) {
// ...
});

request(app);

Die request-Funktion startet nun den übergebenen Webserver beziehungsweise die übergebene Funktion auf einem zufällig ausgewählten Port. Um einen Test ausführen zu können, stellt sie verschiedene Funktionen zur Verfügung, darunter get und post. Ein Aufruf der end-Funktion stellt das Ergebnis der HTTP-Anfrage im Rahmen eines Callbacks zur Verfügung:

request(app)
.get('/foo')
.end(function (err, res) {
// ...
});

Das Ergebnis auswerten

Innerhalb des Callbacks lässt sich die eigentliche Prüfung des Tests unterbringen. Dabei gilt es zu beachten, dass der Test asynchron abläuft. Man muss daher, je nach verwendetem Framework, geeignete Maßnahmen ergreifen: In Mocha muss man dem Test zu diesem Zweck eine done-Funktion übergeben und diese am Ende des Unit-Tests aufrufen.

Die am häufigsten benötigten Parameter wie den HTTP-Statuscode oder die zurückgegebenen Header stellt SuperTest als Eigenschaften am Objekt res zur Verfügung, sodass sich diese leicht prüfen lassen:

var assert = require('node-assertthat'),
express = require('express'),
request = require('supertest');

suite('Express', function () {
var app;

setup(function () {
app = express();
});

test('returns 404 by default for GET /.', function (done) {
request(app)
.get('/')
.end(function (err, res) {
assert.that(res.statusCode, is.equalTo(404));
done();
});
});
});

Die Eigenschaft headers erlaubt den Zugriff auf die zurückgegebenen Header. Zusätzlich verfügt das res-Objekt über eine Reihe von Eigenschaften, die sich zur leichten Analyse von Fehlern nutzen lassen. Dazu zählen unter anderem ok, clientError und serverError, aber auch redirect, accepted, noContent, badRequest, unauthorized, notAcceptable, forbidden und notFound.

Besondere Aufmerksamkeit hat auch die Eigenschaft text des res-Objekts verdient, die den zurückgegebenen Inhalt der Webseite in bereits ausgelesener Form enthält. Auf diesem Weg entfällt also die Notwendigkeit, res zunächst als Stream verarbeiten zu müssen. Möglich ist dies bei Bedarf allerdings weiterhin.

Daten übertragen

Während Daten im Rahmen einer GET-Anfrage vom Client in der Regel als Querystring an den Server übertragen werden, sieht dies bei POST-Anfragen anders aus: Diese übertragen Daten innerhalb der eigentlichen HTTP-Nachricht. Im Zusammenhang mit REST-Diensten wird hierfür üblicherweise JSON als Datenformat verwendet.

Wenn eine Anfrage tatsächlich um JSON-Daten ergänzt werden soll, kann man diese der post-Funktion als zweiten Parameter übergeben. Um alles Weitere kümmert sich SuperTest intern:

request(app)
.post('/', {
foo: 23,
bar: 42
}
)
.end(function (err, res) {
// ...
});

Alternativ steht die attach-Funktion zur Verfügung, um beliebige, nicht als JSON formatierte Daten, an den Webserver zu übertragen:

request(app)
.post('/')
.attach('passwd', '/etc/passwd')
.end(function (err, res) {
// ...
});

Zu guter Letzt lassen sich auch die Header der Anfrage setzen, indem man die set-Funktion aufruft und ihr jeweils einen Schlüssel und einen Wert übergibt, beispielsweise, um den Accept-Header zu definieren:

request(app)
.post('/', { ... })
.set('Accept', 'application/json')
.end(function (err, res) {
// ...
});

SuperAgent als kleiner Bruder

Zwar dient SuperTest nur der einfachen Testbarkeit von Node.js-Servern, allerdings steht dessen Kern unter dem Namen SuperAgent auch als eigenständiges Modul zur Verfügung: Mit diesem ist es möglich, HTTP- und AJAX-Anfragen auch außerhalb von Unit-Tests zu formulieren. Bemerkenswert dabei ist, dass SuperAgent nicht nur unter Node.js, sondern auch im Webbrowser lauffähig ist.

Auf diesem Weg ist es also möglich, sämtliche HTTP-Anfragen stets auf die gleiche Art zu formulieren und eine einheitliche API zu verwenden, gleichwohl, ob man sich im Webbrowser, auf dem Webserver oder innerhalb von Unit-Tests befindet.

tl;dr: SuperTest und dessen kleiner Bruder SuperAgent ermöglichen das komfortable Schreiben von HTTP-Anfragen und ermöglichen deren einfache Auswertung in Unit-Tests. Da sie innerhalb des Webbrowsers, auf dem Webserver und in einer Testumgebung lauffähig sind, erhält man als Entwickler eine einzige, einheitliche API zum Zugriff auf HTTP-Endpunkte.