Top

Testing JavaScript

Das Testen von Code wird mit der Anzahl an Zeilen immer wichtiger, um Fehler zu vermeiden oder besser zu entdecken.
Bei JavaScript ist dieses Thema nun auch ein fester Bestandteil, der Umfang an Möglichkeiten wächst ständig.

Hier ein paar sehr einfache Beispiele auf GitHub: js-test-envs

Ava

Ein Test Runner für JavaScript mit wenig Konfigurationsaufwand.
Er ist sehr einfach zu nutzen und führt die Tests parallel aus.
Jede Test Datei wird in einem eigenen Node Prozess ausgeführt.

Man installiert den Runner global um ava auf der Konsole ausführen zu können.

npm install ava -g

Anschliessend passt man die package.json entsprechend an.

"scripts": {
    "test": "ava --verbose",
    "test:watch": "ava --watch"
}

ES6 Unterstützung ist als Standard eingebaut.

import test from 'ava'

test('Simple', t => {
  t.is('A', 'A')
  t.deepEqual([1, 2], [1, 2])
}

Wenn mehrere Test vorhanden sind hat man mit only die Möglichkeit sich auf einen Test zu konzentrieren und muss nicht alle anderen Test inaktiv setzen.

test.only('Lonely', t => {
  t.is('Alone', 'Alone');
}

Soll ein Test übersprungen werden kann man dies mit skip erzwingen.

Mit pass und fail kann man bestimmen ob ein Test erfolgreich war oder nicht.

test('Test pass', t => {
  t.pass()
}
test('Test fail', t => {
  t.fail()
}

Die bekannten Hooks wie before, beforeEach, after und afterEach sind vorhanden.

Hat man Funktionen, vielleicht auch Callbacks und Promises.

const calculate = (num, amount) => {
    return parseInt(num) + parseInt(amount);
}

const callbackParam = (cb) => {
    cb(null, 'callback test')
};

const resolvedPromise = () => {
    return Promise.resolve('resolved')
};

const rejectedPromise = () => {
    return Promise.reject('rejected')
};

Könnten entsprechende Tests dazu wie folgt aussehen.

import test from 'ava'

test('Calculate', t => {
[
    {input: [1, 0], expected: 1},
    {input: [10, 10], expected: 20},
    {input: ['30', '10'], expected: 40},
    {input: ['30', 40], expected: 70},
].forEach(item => {
        t.is(item.expected, simple.calculate(item.input[0], item.input[1]))
    })
});

test.cb('Callback', t => {
simple.callbackParam((err, value) => {
    t.is(null, err)
    t.is('callback test', value)
    t.end()
});
});

test('Promise resolved', async t => {
    t.is(await simple.resolvedPromise(), 'resolved')
});

test('Promise rejected', async t => {
    const error = await t.throws(simple.rejectedPromise())
    t.is(error, 'rejected');
});

Mocking

Ava hat kein Mocking eingebaut aber Sinon passt gut zu Ava.

npm i sinon --save-dev

Nach der Installation kann es innerhalb der Tests genutzt werden.

import test from 'ava'
import sinon from 'sinon'

const spyFunc = sinon.spy();

test('Function got called', t => {
    spyFunc()
    t.true(spyFunc.calledOnce)
});

Code Coverage

Das Befehlszeilen-Tool nyc für Instanbul funktioniert gut mit Ava.

npm i nyc --save-dev

Die package.json kann dann so erweitert werden.

"scripts": {
    "test": "ava --verbose",
    "test:watch": "ava --watch",
    "coverage": "nyc ava",
    "htmlreport": "nyc report --reporter=html"
}

Jasmine

Ist ein Behavior-Driven Framework zum testen von JavaScript Code.

Jasmine ist ein unabhängiges Framework, ein Browser ist nicht zwingend erforderlich.

Der Aufbau von einem Test descripe hat aber grundsätzlich diese Form, eine Beschreibung und eine Funktion. Die Funktion enthält ein oder mehrere Specs it mit weiteren Beschreibungen und entsprechenden Funktionen. Die eigentlichen Testanweisungen (Expectations) können noch zusätzlich Funktionen (Matcher) angehängt bekommen.

describe("test", function() {
    it("spec checks true", function() {
        expect(true);
        expect(true).toBe(true);
        expect(true).toEqual(true);
    });
});

Die Tests können nach Bedarf verschachtelt werden.

describe("module test", function() {
    describe("method test", function() {
        ...
    });
});

Soll vor und nach jedem Test etwas ausgeführt werden, nutzt man beforeEach() und afterEach().

describe("test", function() {
    let value;
    beforeEach(function() {
        value = true;
    });
    afterEach(function() {
        value = false;
    });
    it("spec checks true", function() {
        expect(value);
        expect(value).toBe(true);
        expect(value).toEqual(true);
    });
});

Jede Testanweisung (Expectations) kann Funktionen (Matcher) angehängt bekommen, um diese zu negieren nutzt man .not.

Wenn die vorhandenen Matcher nicht ausreichend sind kann man sich einfach selber welche schreiben. Selbst geschriebene Matcher werden mit der Funktion addMatchers hinzugefügt. Mit this.actual wird der Paramter der except Funktion angesprochen, weitere Parameter werden der definierten Funktion übergeben.

this.addMatchers({
    toBeObject: function () {
        return this.actual !== null && typeof(this.actual) === 'object';
    }
});

Dann können wir unseren Matcher direkt nutzen.

expect(user).toBeObject();

Jasmine bietet zusätzlich noch Spione (Spies) an. Mit der spyOn Funktion kann eine bestehe Funktion mit einem Spion ersetzt werden. Der Matcher .toHaveBeenCalled() bietet dann die Möglichkeit zu prüfen ob die Funktion aufgerufen worden ist.

describe("test", function() {
    it("spec checks true", function() {
        let user = new User();        
        spyOn(user, "getName");
        let user = user.getFullname();
        expect(user.getName).toHaveBeenCalled();
    });
});

Wenn man an die spyOn eine weitere Funktion anhängt kann man einen bestimmten Wert zurückgeben lassen.

spyOn(user, "getName").andReturn("Name");

Es ist auch möglich die ganze Funktion mit einer anderen Funktion zu ersetzen.

const fakeFunc = function() {
    return "Name";
};
spyOn(user, "getName").andCallFake(fakeFunc);

Zusätzlich zum ersetzen von bestehenden Funktionen kann man mit jasmine.createSpy neue Funktionen erstellen.

user.getEmail = jasmine.createSpy("Email spy");
user.getEmail();
expect(user.getEmail).toHaveBeenCalled();

Wie bei spyOn können weitere Funktionen genutzt werden.

user.getEmail = jasmine.createSpy("Email spy").andReturn("Email");

user.getEmail = jasmine.createSpy("Email spy").andCallFake(function() {
    return "Email";
});

Sinon

Ist gedacht um Test Stubs and Mocks für JavaScript zu nutzen.

Mocks

Platzhalter für echte Objekte innerhalb von Modultests.

'test should call subscriber': () => {   
    const myAPI = { method: () => {} };
    const mock = sinon.mock(myAPI);

    mock.expects('method').once();
}

Spys

Spy properties: - spy.called - spy.calledOnce - spy.calledTwice - spy.calledThrice - spy.callCount

'test should call subscriber': () => {   
    const spy = sinon.spy();
}

Stubs

'test should call subscriber': () => {   
    const callback = sinon.stub();

    callback.withArgs(42).returns(1);
    callback.withArgs(1).throws("TypeError");
}

Mocha

Ein simples flexibles Framework für NodeJS.

npm install -g mocha

Mocha ist ein JavaScript Test Framework für NodeJS oder dem Browser.

Nach der Installation kann Mocha einfach auf der Konsole ausgeführt werden.

mocha test.js

Zusammen mit assert kann man nun Tests schreiben.

const assert = require('assert');
describe('List object', function() {
    describe('find', function() {
        it('search list object', function() {
            assert(-1, [1, 2, 3].indexOf(5));
        });
    });
});

Um Mocha im Browser zu nutzen erstellt man sich eine HTML Datei. Die benötigten Dateien mocha.css und mocha.js kann man hier herunterladen.

<html>
    <head>
        <meta charset="utf-8">
        <title>Mocha Tests</title>
        <link rel="stylesheet" href="mocha.css" />
        <script src="mocha.js"></script>
        <script>mocha.setup('bdd')</script>
        <script>
        function assert(expr, msg) {
            if (!expr) throw new Error(msg || 'failed');
        }
        </script>
        <script src="test.js"></script>
        <script>
        window.onload = function() {
            var runner = mocha.run();
        };
        </script>
    </head>
    <body>
        <div id="mocha"></div>
    </body>
</html>

Im Browser steht require nicht zur Verfügung, darum muss man eine assert Funktion definieren und den Aufruf entfernen.

describe('List object', function() {
    describe('find', function() {
        it('search list object', function() {
            assert(-1, [1, 2, 3].indexOf(5));
        });
    });
});

Frisby

Ein REST API Testing Framework baut auf NodeJS und Jasmine auf.

SlimerJS

Ein Gecko mit JavaScript API der ohne vorhandenen Browser funktioniert.

CasperJS

Setzt auf PhantomJS auf bietet die Möglichkeit ein Browserverhalten zu simulieren.

CasperJS bietet die Möglichkeit eine Webseite komplett auf der Konsole zu testen. Es kann mit PhantomJS oder mit SlimerJS genutzt werden und bietet zusätzlich nützliche Funktionen um Webseiten zu untersuchen.

CasperJS wird nach der Installation einfach auf der Konsole ausgeführt.

casperjs --version
1.1.0-beta3

Um zu testen ob alles funktioniert bringt CapserJS einen Selbsttest mit.

casperjs selftest

Eigene Tests werden einfach als Argument übergeben.

casperjs script.js

Als erstes erzeugt man eine casper Instanz.

const casper = require('casper').create({
    verbose: true,
    viewportSize: {
        width: 800, 
        height: 600
    },
    pageSettings: {
        loadImages:  false
    },
    logLevel: "debug"
});

Anschliessend kann man mit der start() Methode ein URL öffnen.

casper.start('http://dbproductions.de/', function() {
    this.echo(this.getCurrentUrl(), 'INFO');
    this.echo(this.getTitle());
});

casper.run(function() {
    this.echo('Done.').exit();
});

Die then() Methode ist optional, um n Schritte einer Navigation zu testen.

casper.start('http://dbproductions.de/');

casper.then(function() {
    this.echo(this.getCurrentUrl(), 'INFO');
    this.echo(this.getTitle());
    if (!this.exists('.error')) {
        this.echo('There is no element with an error class.');
    }
});

casper.run(function() {
    this.echo('Done.').exit();
});

CasperJS nutzt CSS Selektoren um auf die Elemente des DOM zuzugreifen.

this.echo(this.fetchText('h1'));
this.echo(this.getElementAttribute('a', 'href'));

Es werden eine Vielzahl von Methoden geboten um mit einer Webseite zu interagieren.

this.reload(function() {
    this.echo("page gets reloaded");
});

CasperJS hat ein tester Modul um Tests zu schreiben.

casperjs test test.js

Die vorkonfigurierte Instanz kann in der Test Umgebung nicht uberschrieben werden.

casper.test.begin('Homepage test suite', 1, function suite(test) {
    casper.start('http://dbproductions.de/', function() {
        this.echo(this.getCurrentUrl(), 'INFO');
        test.assertTitle('DBProductions | 我的新技术博客', 'check startpage title');
        this.clickLabel('JavaScript', 'a');
    });
    casper.run(function() {
        test.renderResults(true);
    });
});

Die Testergebnisse werden auf der Konsole ausgegeben, können auch exportiert und in einer xUnit XML Datei gespeichert werden.

casper.run(function() {
    test.renderResults(true, 0, 'testlog.xml');
});

JSCheck

Ein Testing-Tool für JavaScript von Douglas Crockford.

Das Testing-Tool erstellt zufällige Test Cases gegenüber bestimmten Eigenschaften. jscheck.js stellt das JSC Objekt zur Verfügung welches die benötigten Methoden besitzt.

<html>
    <head>
        <title>JSCheck</title>
        <meta charset="utf-8">
        <script src="jscheck.js"></script>
    </head>
    <body>
        <script>
        JSC.on_report(function(report) {
            console.info(report);
        });
        JSC.on_fail(function(report) {
            console.info('fail', report.name, report.args);
        });

        function testFunc(a, b, c) {
            return a + b <= c;
        }

        JSC.test('Test Case', function (verdict, a, b, c) {
            return verdict(testFunc(a, b, c));
        }, [JSC.integer(1, 10), JSC.integer(1, 10), 10]);
        </script>
    </body>
</html>

Es werden 100 Test Cases erstellt, dies ist der Standardwert.

JSC.reps(10);

Um JSCheck mit NodeJS zu nutzen muss man diese Zeile am Ende von jscheck.js einfügen.

(typeof window !== 'undefined' ? window : exports).JSC = JSC;

Dann kann man sich das HTML sparen.

var JSC = require('./jscheck').JSC;

JSC.on_report(function(report) {
    console.log(report);
});

JSC.on_fail(function(report) {
    console.log('fail', report.name, report.args);
});

function testFunc(a, b, c) {
    return a + b <= c;
}

JSC.test('Test Case', function (verdict, a, b, c) {
    return verdict(testFunc(a, b, c));
},[JSC.integer(1, 10), JSC.integer(1, 10), 10]);

ESLint

Ist ein Code Analyse Tool für JavaScript und JSX.