REST API testen mit Node.js, Mocha und Chakram - auch für Java Entwickler

von Peter Soth

In diesem Blog-Post möchte ich aufzeigen, wie man Node.js [1], Mocha [2] und Chakram [3] für das Testen von REST APIs benutzt und warum Selbiges auch für Java-Entwickler, die eigentlich gar nichts mit Node.js am Hut haben, interessant sein kann.

So benutzen wir JUnit für Unit-Tests und das Gespann Geb / Spock für Integrations- bzw. Funktionale Tests. Diese werden entweder während der Programmierung innerhalb von IntelliJ gestartet oder über den täglichen Build mittels Jenkins (Continuous Integration) ausgeführt.

Sollen die Tests jedoch direkt in der Infrastruktur des Kunden laufen, so scheitert dieser Ansatz. Die funktionalen Tests des Web-Frontends kann man noch per Testspezifikation und manuellen Tests abdecken, aber wie sieht es bei einer REST API aus? Wollte man hier händisch vorgehen, könnte man dies mit vorbereiteten Testdaten bewerkstelligen, die zusammen mit cURL Aufrufen benutzt werden. Geht das nicht auch automatisiert?

Folgende Eigenschaften sollte ein automatisiertes Framework erfüllen:

  • Das Framework sollte nicht aus dem Java-Stack kommen. Hierdurch kann man sicherstellen, dass sich nicht potentielle Java-Fehler in die REST-Schnittstelle »einschleichen«.
  • Es sollte auf unterschiedlichen Betriebssystemen unkompliziert installierbar sein. So fand ich für Python Lösungen, diese waren jedoch unter Windows nicht einfach aufzusetzen.
  • Eine Validierung von komplexen Datentypen wie UUIDs oder Datumswerte nach ISO 8601 [4] sollte möglich sein.
  • Die Formulierung der Testfälle sollte simpel und deklarativ sein. 
  • System-Tests sollen möglich sein, sprich die REST-API sollte nicht nur innerhalb des Tomcats getestet werden, sondern auch über den als Load-Balancer vorgeschalteten Apache-Web-Server.

Schließlich bin ich bei Chakram im Kontext von Node.js und Mocha fündig geworden. Eigentlich wollte ich nicht mit Node.js und Javascript arbeiten, aber nach einer kurzen Einarbeitungszeit fand ich das Ganze nicht mehr so schlecht und das Validator-Package [5] von Node.js nahm mir schlussendlich sehr viel Arbeit bei der Validierung ab.

Die Modularisierung und Wiederverwendung von Javascript-Code innerhalb von Node.js kostete mich dann doch etwas Zeit, deshalb folgt im Souce-Code unten meine Utility-Methode validateHTTPMethods().

var chakram = require('chakram'),
    expect = chakram.expect;

module.exports = {
    validateHTTPMethods: function(loggedin, httpMethod, host, url, httpCode) {

        it("validate HTTP method " + httpMethod + "  " + url, function() {
            var loginToken;

            return chakram.post(host + "/webapp/rest/login", {
                "username": "user1",
                "password": "password"
            }).then(function(loginResponse) {
                expect(loginResponse).to.have.json('access_token', function(token) {
                    expect(token).to.have.length(32);
                    if (!loggedin) {
                        loginToken = 'nologintoken';
                    } else {
                        loginToken = token;
                    }
                });
                return chakram.request(httpMethod, host + url, {
                    headers: {
                        'Content-Type': 'text/json',
                        'Authorization': 'Bearer ' + loginToken
                    }
                }).then(function(docResponse) {
                    expect(docResponse).to.have.status(httpCode);
                });
            });
        });
    }
};

Mittels require wird die utility.js dann in den Test-Cases benutzt. Des Weiteren sieht man in folgendem Source-Code sehr schön wie einfach und deklarativ Test-Cases geschrieben werden können:

var config = require('../config.json');
var schema = require('../jsonschemas.json');
var utility = require('../utility.js');
var validator = require('validator');

var chakram = require('chakram'),
    expect = chakram.expect;

describe("Testing Pull Document", function() {
    var loginToken;

    before("test login token creation", function() {
        ...
    });

    utility.validateHTTPMethods(false, "GET", config.server, "/rest/document", 401);
    utility.validateHTTPMethods(true, "GET", config.server, "/rest/document", 200);
    utility.validateHTTPMethods(true, "POST", config.server, "/rest/document", 405);
    utility.validateHTTPMethods(true, "PUT", config.server, "/rest/document", 405);
    utility.validateHTTPMethods(true, "DELETE", config.server, "/rest/document", 405);
    utility.validateHTTPMethods(true, "HEAD", config.server, "/rest/document", 200);

    describe("testing JSON", function() {
        var lastUpdated;
        var response;
        before(function() {
            response = chakram.request("GET", config.server + "/rest/document", {
                headers: {
                    'Content-Type': 'text/json',
                    'Authorization': 'Bearer ' + loginToken
                }
            });
            expect(response).to.have.status(200);
            expect(response).to.have.header('content-type', 'application/json;charset=UTF-8');
            expect(response).to.have.json('data[0].lastUpdated', function(last) {
                lastUpdated = last;
                expect(validator.isISO8601(lastUpdated)).to.be.true;
            });
        });

        it("test pull document if returned JSON is valid", function() {
            // validate JSON schema
            expect(response).to.have.schema(schema.document);
            // validate JSON data
            expect(response).to.have.json('data[0].id', function(id) {
                expect(validator.isUUID(id)).to.be.true;
            });
            expect(response).to.have.json('data[0].class', 'de.exensio.Document');
            expect(response).to.have.json('data[0].dateCreated', function(created) {
                expect(validator.isISO8601(created)).to.be.true;
            });
            expect(response).to.have.json('data[0].deleted', function(deleted) {
                expect(validator.isBoolean(deleted)).to.be.true;
            });
            expect(response).to.have.json('data[0].fileName', function(fileName) {
                expect(fileName).not.to.be.empty;
                expect(fileName).to.be.a('string');
            });
            expect(response).to.have.json('data[0].lastUpdated', function(updated) {
                expect(validator.isISO8601(updated)).to.be.true;
            });
            expect(response).to.have.json('data[0].mimeType', function(mimeType) {
                expect(mimeType).not.to.be.empty;
            });
            expect(response).to.have.json('data[0].size', function(size) {
                expect(validator.isInt(size)).to.be.true;
            });
            expect(response).to.have.json('data[0].file', function(file) {
                expect(file).not.to.be.empty;
            });
            return chakram.wait();
        });
    });
});

Fazit

Meines Erachtens eignet sich das Gespann Node.js, Mocha und Chakram sehr gut für das Aufsetzen von deklarativen und kompakten Testcases. Vor allem das Validator-Plugin ermöglicht die einfache Validierung von unzähligen Datentypen.

Kategorien: Node JSTesting

Zurück