chevron-left chevron-right

[JS] Testowanie kodu JavaScript za pomocą Gulp.js, Karma i Mocha.js

W dzisiejszych czasach testowanie kodu JavaScript jest niezbędne. Dzięki przetestowanemu kodowi jesteśmy zabezpieczeni, że jakiekolwiek przyszłe zmiany w kodzie i problemy z nimi związane, zostaną szybko wykryte i zmuszą programistą do przemyślenia swojego kodu i aktualizacji scenariuszy testowych.

Scenariusze testów stanowią swoistego rodzaju poradnik dotyczący tego jak działa kod, jak używać dany kawałek kodu oraz czego się można po nim spodziewać. Testując nasz kod, możemy wykryć kod, który jest zbędny, a który został dodany na jakimś etapie rozwoju oprogramowania, bo mieliśmy jakąś koncepcję na rozwiązanie problemu, która ostatecznie nie została wdrożona.

Mając przygotowane testy jednostkowe JavaScript, warto się pokusić o przygotowanie informacji dotyczącej tego ile naszego kodu zostało pokryte testami, tzw. code coverage. Dzięki temu, mamy podpowiedź dotyczącą kodu, który mógł jeszcze nie zostać przetestowany.

Warto przypomnieć, że nasze testy jednostkowe będą uruchamiane na node.js. Dlatego najpierw musisz go zainstalować na swoim komputerze.

Przykładowy kod pluginu JavaScript

Nasz przykładowy kod, będzie bardzo prostym pluginem, który będzie pozwalał na jakiś stopień konfiguracji przez innych programistów korzystających z naszego rozwiązania. Nasz kod będzie pełnił rolę pluginu, który będzie pozwalał nam zdefiniować, które elementy mają być klikalne. W przypadku naszego pluginu, po kliknięciu w jakiś element będzie pojawiał się tekst w kontenerze logów (i ewentualnie animacja pojawienia się tekstu).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// clicker.js
(function (window, document) {
    'use strict';
 
    window.Clicker = function (customParams) {
        var CLASS_LOGS = 'logs',
            CLASS_LOGS_ANIMATABLE = CLASS_LOGS + '--animatable',
            CLASS_CLICKER = 'clicker',
            CLASS_CLICKER_READY = CLASS_CLICKER + '--ready',
            SELECTOR_CLICKER = '.' + CLASS_CLICKER,
            logsContainer = document.createElement('div'),
            params = {
                selector: SELECTOR_CLICKER,
                isAnimatable: false
            },
            transitions = {
                transition: 'transitionend',
                OTransition: 'oTransitionEnd',
                MozTransition: 'transitionend',
                WebkitTransition: 'webkitTransitionEnd'
            },
            paramKey,
            elements,
            detectedTransitionEvent,
            transitionKey,
            clickCallback = function (event) {
                var text = document.createElement('p'),
                    triggeredOpacity;
 
                text.innerHTML = 'Click!';
                logsContainer.appendChild(text);
 
                if (params.isAnimatable) {
                    text.style.opacity = 0;
                    triggeredOpacity = window.getComputedStyle(text).opacity;
                    text.style.opacity = 1;
                }
            },
            transitionEndCallback = function (event) {
                var text = document.createElement('p');
 
                text.innerHTML = 'Transition finished!';
                logsContainer.appendChild(text);
            };
 
        for (paramKey in customParams) {
            if (customParams.hasOwnProperty(paramKey)) {
                params[paramKey] = customParams[paramKey];
            }
        }
 
        for (transitionKey in transitions) {
            if (transitions.hasOwnProperty(transitionKey) && logsContainer.style[transitionKey] !== undefined) {
                detectedTransitionEvent = transitions[transitionKey];
            }
        }
 
        logsContainer.classList.add(CLASS_LOGS);
        document.body.appendChild(logsContainer);
 
        elements = [].slice.call(document.querySelectorAll(params.selector));
 
        elements.forEach(function (element) {
            element.addEventListener('click', clickCallback, false);
            element.classList.add(CLASS_CLICKER_READY);
        });
 
        if (params.isAnimatable) {
            logsContainer.classList.add(CLASS_LOGS_ANIMATABLE);
 
            if (detectedTransitionEvent) {
                logsContainer.addEventListener(detectedTransitionEvent, transitionEndCallback, false);
            }
        }
    };
})(window, window.document);

Przygotowanie środowiska testowego dla testów jednostkowych JavaScript

Całe środowisko dla testów jednostkowych (unit testów) będzie oparte o następujące narzędzia:

  1. Gulp.js,
  2. Karma,
  3. Mocha i Chai.

1. Gulp.js - narzędzie do automatyzacji zadań

Swego czasu na blogu pisałem o innym narzędziu, które służy do automatyzacji zadań webdeveloperskich - o Grunt.js. Tym razem wykorzystam inne narzędzie, które robi to samo, ale w inny sposób. Główne różnice między tymi dwoma narzędziami to:

  • W Grunt.js konfiguracja jest dostarczana jako obiekt JSON, natomiast w Gulp.js konfigurujemy pisząc kod JavaScript. To sprawia, że na pierwszy rzut oka, Grunt.js jest łatwiejszy w konfiguracji.
  • Gulp.js wykonuje swoje zadania w oparciu o funkcjonalność node.js o nazwie Stream, która pozwala przetwarzać dane bezpośrednio w pamięci bez zapisywania stanów pośrednich na dysku. Grunt.js poszczególne wyniki przetwarzania plików w poszczególnych etapach zapisuje jako pliki tymczasowe na dysku. Takie podejście sprawia, że Gulp.js jest z reguły szybszy niż Grunt.js.

Gulp.js można zainstalować za pomocą następującej komendy:

npm install gulp --save-dev

Przydatna będzie też linia komend do Gulpa, którą instalujemy za pomocą komendy:

npm install gulp-cli -g

2. Mocha + Chai - framework do testowania kodu JS

Mocha.js jest frameworkiem do pisania testów jednostkowych w JavaScript, który pozwala uruchamiać testy zarówno w przeglądarce jak i na serwerze node.js z poziomu terminala. Dodatkowo, framework Mocha.js jest rozszerzony o bibliotekę Chai.js, która służy pisania warunków sprawdzających w trybie TDD/BDD. Dzięki takiemu zestawowi narzędzi, nasz kod testowy będzie łatwy do pisania i przyjemny do czytania.

npm install mocha chai chai-dom --save-dev

3. Karma - środowisko uruchomieniowe dla testów

Karma jest środowiskiem uruchomieniowym dla testów, to oznacza, że z pomocą tego narzędzia możemy uruchamiać testy z poziomu konsoli bez potrzeby uruchamiania przeglądarki. Dzięki integracji Gulp.js i Karma jesteśmy w stanie szybko dostać informację o stanie naszych testów jednostkowych.

Do narzędzia Karma istnieje szereg pluginów i dodatków które pozwalają nam skonfigurować to narzędzie. W naszym przypadku wykorzystamy następujący zestaw rozszerzeń:

  • karma-mocha,
  • karma-mocha-reporter,
  • karma-chai,
  • karma-chai-plugins,
  • karma-coverage,
  • karma-phantomjs-launcher.

Karma i jego dodatki możemy zainstalować za pomocą następującej komendy:

npm install --save-dev karma karma-mocha karma-mocha-reporter karma-chai karma-chai-plugins karma-coverage karma-chrome-launcher karma-firefox-launcher karma-phantomjs-launcher

Podobnie jak w przypadku Gulp.js przyda się również linia komend do Karmy:

npm install karma-cli -g

Konfiguracja testów w Gulp.js

Pierwszym krokiem który wykonamy będzie urochomienie konfiguratora Karmy za pomocą komendy:

karma init

Z jego pomocą określimy w której przeglądarce będziemy testować nasz kod JS, z którego frameworka testowego będziemy korzystać oraz kilka innych rzeczy. Na koniec powinniśmy otrzymać plik z konfiguracją:

// karma.conf.js
// Karma configuration
// Generated on Wed Dec 30 2015 11:40:36 GMT+0100 (CET)

'use strict';

module.exports = function (config) {
    config.set({
        // base path that will be used to resolve all patterns (eg. files, exclude)
        basePath: '',

        // frameworks to use
        // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
        frameworks: [
            'mocha',
            'chai',
            'chai-dom'
        ],

        // list of files / patterns to load in the browser
        files: [
            'clicker.js',
            'test/clicker-tests.js'
        ],

        // list of files to exclude
        exclude: [],

        // preprocess matching files before serving them to the browser
        // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
        preprocessors: {
            'clicker.js': ['coverage']
        },

        // test results reporter to use
        // possible values: 'dots', 'progress'
        // available reporters: https://npmjs.org/browse/keyword/karma-reporter
        reporters: [
            'mocha',
            'coverage'
        ],

        coverageReporter: {
            type: 'html',
            dir: 'test/coverage/'
        },

        // web server port
        port: 9876,

        // enable / disable colors in the output (reporters and logs)
        colors: true,

        // level of logging
        // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
        logLevel: config.LOG_INFO,

        // enable / disable watching file and executing tests whenever any file changes
        autoWatch: true,

        // start these browsers
        // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
        browsers: ['PhantomJS'],

        // Continuous Integration mode
        // if true, Karma captures browsers, runs the tests and exits
        singleRun: false,

        // Concurrency level
        // how many browser should be started simultaneous
        concurrency: Infinity
    });
}

Wygenerowany plik konfiguracyjny musimy uzupełnić o brakujące frameworki testowe: chai oraz chai-dom, a także musimy podać odpowiednie ścieżki do pliku z testami oraz do samego pliku z pluginem. Dodatkowo dorzucamy konfigurację dla pokrycia kodu (coverage).

Kolejnym krokiem będzie przygotowanie pliku Gulp.js, który będzie uruchamiał testy:

1
2
3
4
5
6
7
8
9
10
var gulp = require('gulp'),
    Server = require('karma').Server,
    gutil = require('gulp-util');
 
gulp.task('test', function (done) {
    new Server({
        configFile: __dirname + '/karma.conf.js',
        singleRun: true
    }, done).start();
});

Kod jest bardzo krótki. Za jego pomocą tworzymy zadanie w Gulp.js które korzysta z narzędzia Karma i do którego przekazujemy ścieżkę do pliku konfiguracyjnego do Karmy.

Tak przygotowany zestaw uruchamiamy za pomocą komendy:

gulp test

W konsoli rezultat testu może wyglądać następująco (zdjęcie z testów dla innego pluginu):

Gulp.js Mocha Karma - JS testy jednostkowe frontend

Automatycznie zostanie wygenerowany plik HTML, który pokaże nam stopień pokrycia kodu testami jednostkowymi. Plik będzie dostępny w test/coverage/PhantomJS 1.9.8 (Mac OS X 0.0.0)/index.html. Po otwarciu tego pliku w przeglądarce internetowej zobaczymy stronę z różnymi statystykami, która może wyglądać podobnie do tego co jest na zdjęciu poniżej:

Gulp.js Mocha Karma - JS testy jednostkowe frontend - pokrycie kodu

Podsumowanie

Zdecydowałem się napisać ten artykuł ze względu na fakt, że trudno znaleźć informacje o tym jak testować czysty kod JS, który będzie testował zachowanie pluginu w oknie przeglądarki. Większość przykładów najczęściej dotyczy konfiguracji testów z uwzględnieniem frameworków takich Angular.js, Backbone.js, itd. itp.

Mam nadzieję, że ten artykuł okaże się przydatny dla Ciebie i pomoże podnieść jakość Twojego kodu JS poprzez przygotowania środowiska testowego w którym będziesz mógł/mogła uruchamiać przygotowane przez Ciebie testy jednostkowe w JavaScript.

Jeśli jesteś zainteresowany/a tematyką testów w JavaScript, to daj znać w komentarzach pod tym wpisem. Postaram się wtedy przygotować więcej artykułów dotyczących tego tematu.

  • Jak dla mnie w artykule zabrakło najważniejszej części: jak napisać dobre testy 😉

    Bawiłem się kiedyś tandemem Mocha + Karma, po czym przerzuciłem się na Intern.js. Niezwykle spasiona kobyła od Dojo, która robi naprawdę wszystko, ale równocześnie jest strasznie upierdliwa (AMD everywhere…). Dlatego teraz z zaciekawieniem przyglądam się Bender.js, którego używamy w CKE. Niemniej potrzebuje on jeszcze sporo miłości… I teraz mam dylemat: użerać się z Intern.js czy dopieścić Bendera 😉

    Tak, wiem, łatwiej jest wrócić do Karmy i Mochy, ale… to po prostu nie w moim stylu. No i Intern.js i Bender.js mają kilka zalet, których nie mają inne frameworki (np. Bender.js ma fajnie rozwiązane testy manualne, a Intern.js… no, po prostu robi wszystko – oprócz właśnie testów manualnych).

  • Pierwotnie, artykuł miał zawierać część dotyczącą pisania testów w Mocha+Chai ale zrezygnowałem z tego. To będzie w następnym artykule. Nie chciałem robić jednego bardzo długiego wpisu.