JS testing. Karmic Webpack

    image

    Hello!

    A couple of months ago I wrote a post on how to teach webpack for spa
    From that moment, the tool stepped forward and overgrown with an additional number of plug-ins, as well as examples of configurations.

    In this article I want to share the experience of mixing the explosive mixture webpack + jasmine + chai + karma.

    The best, in my opinion, book about automated testing by Christian Johansen - Test-Driven JavaScript Development - identifies problems that the developer encounters when writing code without tests:

    - The code is written, but the behavior is not available in the browser (example .bind () and IE 8);
    - Implementation is changed, but the combination of components leads to erroneous or not working functionality;
    - The new code is written, you need to take care of the behavior with the old interfaces.

    Based on experience, I will say.
    Programmers who choose the samurai path of TDD (Test-driven development) spend a lot of time covering code with tests. As a result, they benefit from testing and catching bugs.

    Glossary


    Webpack - a modular asset collector;
    - Karma - test-runner for JavaScript;
    - Jasmine - a tool for defining tests in the style of BDD;
    - Chai - a library for checking conditions, expect , assert , should;

    Package Installation


    To begin with, I’ll give a list of packages that are additionally installed in the project. We will use npm for this .

    #tools
    npm i chai mocha phantomjs-prebuilt --save-dev
    #karma packages #1
    npm i karma karma-chai karma-coverage karma-jasmine --save-dev
    #karma packages #2
    npm i karma-mocha karma-mocha-reporter karma-phantomjs-launcher --save-dev
    #karma packages #3
    npm i karma-sourcemap-loader karma-webpack --save-dev

    Move on.

    Environment setting


    After installing additional packages, configure karma. To do this, create the file karma.conf.js in the root of the project

    touch karma.conf.js

    With the following content:

    // karma.conf.js
    var webpackConfig = require('testing.webpack.js');
    module.exports=function(config) {
    config.set({
        // конфигурация репортов о покрытии кода тестами
        coverageReporter: {
          dir:'tmp/coverage/',
          reporters: [
            { type:'html', subdir: 'report-html' },
            { type:'lcov', subdir: 'report-lcov' }
          ],
          instrumenterOptions: {
            istanbul: { noCompact:true }
          }
        },
        // spec файлы, условимся называть по маске **_*.spec.js_**
        files: [
            'app/**/*.spec.js'
        ],
        frameworks: [ 'chai', 'jasmine' ],
        // репортеры необходимы для  наглядного отображения результатов
        reporters: ['mocha', 'coverage'],
        preprocessors: {
            'app/**/*.spec.js': ['webpack', 'sourcemap']
        },
        plugins: [
            'karma-jasmine', 'karma-mocha',
            'karma-chai', 'karma-coverage',
            'karma-webpack', 'karma-phantomjs-launcher',
            'karma-mocha-reporter', 'karma-sourcemap-loader'
        ],
        // передаем конфигурацию webpack
        webpack: webpackConfig,
        webpackMiddleware: {
          noInfo:true
        }
      });
    };

    Configuring webpack:

    // testing.webpack.js
    'use strict';
    // Depends
    var path = require('path');
    var webpack = require('webpack');
    module.exports = function(_path) {
      var rootAssetPath = './app/assets';
      return {
        cache: true,
        devtool: 'inline-source-map',
        resolve: {
          extensions: ['', '.js', '.jsx'],
          modulesDirectories: ['node_modules']
        },
        module: {
          preLoaders: [
            {
              test: /.spec\.js$/,
              include: /app/,
              exclude: /(bower_components|node_modules)/,
              loader: 'babel-loader',
              query: {
                presets: ['es2015'],
                cacheDirectory: true,
              }
            },
            {
              test: /\.js?$/,
              include: /app/,
              exclude: /(node_modules|__tests__)/,
              loader: 'babel-istanbul',
              query: {
                cacheDirectory: true,
              },
            },
          ],
          loaders: [
            // es6 loader
            {
              include: path.join(_path, 'app'),
              loader: 'babel-loader',
              exclude: /(node_modules|__tests__)/,
              query: {
                presets: ['es2015'],
                cacheDirectory: true,
              }
            },
            // jade templates
            { test: /\.jade$/, loader: 'jade-loader' },
            // stylus loader
            { test: /\.styl$/, loader: 'style!css!stylus' },
            // external files loader
            {
              test: /\.(png|ico|jpg|jpeg|gif|svg|ttf|eot|woff|woff2)$/i,
              loader: 'file',
              query: {
                context: rootAssetPath,
                name: '[path][hash].[name].[ext]'
              }
            }
          ],
        },
      };
    };

    We are ready to write and run the first test.

    Definition of spec files


    image
    Experience shows that specs (from the English spec - specification) are more convenient to store in the same folders as the tested components. Although, of course, you yourself build the architecture of your application. In the example below, you will find the only test example for the trial article, which is located in the tests directory of the boilerplate module .

    Such directory naming gives a positive response from new developers who want to get acquainted with the functionality of a module or component.

    TL; DR opening the project, we see the folder with specifications, located in first place due to string sorting.

    Launch


    Nothing new here.
    To start, I use the built-in npm functionality of the scripts section.
    Exactly the same as for the dev-server and the "battle" build functionality.

    In package.json, we declare the following commands:

    "scripts": {
        ...
        "test:single": "rm -rf tmp/ && karma start karma.conf.js --single-run --browsers PhantomJS",
        "test:watch": "karma start karma.conf.js --browsers PhantomJS"
        ...
     }

    To run the tests in the "update on change" mode, at the root of the project we type the command:

    npm run test:watch

    For a one-time launch:

    npm run test:single

    image

    First test


    For example, I propose to consider a task that is not trivial in terms of unit testing. Processing the result of Backbone.View.
    It's okay if the first test looks like a formality.

    Consider the View code:

    // view.js
    module.exports = Backbone.View.extend({
      className: 'example',
      tagName: 'header',
      template: require('./templates/hello.jade'),
      initialize: function($el) {
        this.$el = $el;
        this.render();
      },
      render: function() {
        this.$el.prepend(this.template());
      }
    });

    It is expected that when you create an instance of View, the render () function will be called. The result of which will be html - declared in the hello.jade template

    Example of a formal test covering functionality:

    // boilerplate.spec.js
    'use strict';
    const $ = require('jquery');
    const Module = require('_modules/boilerplate');
    describe('App.modules.boilerplate', function() {
      // подготовим переменные для использования
      let $el = $('
    ', { class: 'test-div' });   let Instance = new Module($el); // формальная проверка на тип возвращаемой переменной   it('Should be an function', function() {     expect(Module).to.be.an('function');   }); // после применения new на функции конструкторе - получим объект   it('Instance should be an object', function() {     expect(Instance).to.be.an('object');   }); // инстанс должен содержать el и $el свойства   it('Instance should contains few el and $el properties', function() {     expect(Instance).to.have.property('el');     expect(Instance).to.have.property('$el');   }); // а так же ожидаем определенной функции render()   it('Instance should contains render() function', function() {     expect(Instance).to.have.property('render').an('function');   }); // $el должен содержать dom element   it('parent $el should contain rendered module', function() {     expect($el.find('#fullpage')).to.be.an('object');   }); });

    We start testing and observe the result.
    image

    In addition to everything, the tmp / coverage / html-report / directory will contain a code coverage report:
    image

    Conclusion


    Tests, even in such a formal form, will relieve us of our obligations to ourselves.
    Using sufficient ingenuity in their declaration, we can protect ourselves and our colleagues from headaches.

    In conclusion, imagine the amount of time that we spend daily on each iteration: "changed - saved - updated the browser - saw the result."
    The obvious is near. Testing is a useful tool to guard your time.

    Example


    See this link webpack-boilerplate

    Thank you for reading.

    Also popular now: