image

Привет!

Пару месяцев назад я писал пост, о том как научить webpack для spa
С того момента инструмент шагнул вперед и оброс дополнительным количеством плагинов, а так же примерами конфигураций.

В этой статье хочу поделиться опытом смешивания гремучей смеси webpack + jasmine + chai + karma.

В лучшей, по-моему мнению, книге про автоматизированное тестирование Christian Johansen - Test-Driven JavaScript Development – обозначены проблемы, с которыми разработчик сталкивается при написании кода без тестов:

– Код написан, но поведение не доступно в браузере (пример .bind() и IE 8);
– Имплементация изменена, но совокупность компонентов приводит к ошибочному или не рабочему функционалу;
– Новый код написан, нужно позаботиться о поведении со старыми интерфейсами.

Опираясь на опыт, скажу.
Программисты, избравшие путь самурая TDD (Test-driven development ), тратят много времени на покрытие кода тестами. В итоге остаются в выигрыше на этапе тестирования и отлавливания багов.

Глоссарий


– Webpack — модульный сборщик ассетов;
Karma — test-runner для JavaScript;
Jasmine — инструмент для определения тестов в стиле BDD;
Chai — библиотека для проверки условий, expect, assert, should;

Установка пакетов


Для начала, приведу список пакетов, которые дополнительно устанавливаем в проект. Для этого воспользуемся npm.

#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

Идем дальше.

Настройка окружения


После установки дополнительных пакетов, настраиваем конфигурацию karma. Для этого в корне проекта создадим файл karma.conf.js

touch karma.conf.js

Со следующим содержанием:

// 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
    }
  });
};

Конфигурирование 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]'
          }
        }
      ],
    },
  };
};

Мы готовы к написанию и запуску первого теста.

Определение spec файлов


image
Опыт показывает, что спеки (от англ spec — specification) удобнее хранить в тех же папках, что и тестируемые компоненты. Хотя, конечно же, Вы сами строите архитектуру своего приложения. В примере ниже, Вы встретите единственный для ознакомительной статьи пример теста, который расположен в директории tests модуля boilerplate.

Такое именование директорий дает позитивный отклик от новых разработчиков, желающих ознакомиться с функционалом модуля или компонента.

TL;DR открывая проект, мы видим папку со спецификациями, расположенную на первом месте за счет строковой сортировки.

Запуск


Тут ничего нового.
Для старта я использую встроенный функционал npm секции scripts.
Ровно так же как и для dev-server и "боевой" сборки функционала.

В package.json объявляем следующие команды:

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

Чтобы запустить тесты в режиме "обновляй при изменении", в корне проекта набираем команду:

npm run test:watch

Для разового запуска:

npm run test:single

image

Первый тест


Для примера, предлагаю рассмотреть нетривиальную с точки зрения unit тестирования задачу. Обработка результата работы Backbone.View.
Ничего страшного, если первый тест выглядит формальностью.

Рассмотрим код View:

// 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());
  }
});

Ожидается, что при создании экземпляра View, будет вызвана функция render(). Результатом которой станет html – декларированный в шаблоне hello.jade

Пример формального теста покрывающего функционал:

// boilerplate.spec.js
'use strict';

const $ = require('jquery');
const Module = require('_modules/boilerplate');

describe('App.modules.boilerplate', function() {
  // подготовим переменные для использования
  let $el = $('<div>', { 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');
  });
});

Запускаем тестирование и наблюдаем за результатом.
image

В дополнении ко всему, директория tmp/coverage/html-report/ будет содержать отчет о покрытии кода:
image

Вывод


Тесты, даже в таком формальном виде, избавят нас от обязательств перед собой.
Применив достаточную изобретательность в их декларации, мы можем уберечь себя и коллег от головной боли.

В заключении, представьте то количество времени, которое мы ежедневно тратим на каждую итерацию: "изменил – сохранил – обновил браузер – увидел результат".
Очевидное рядом. Тестирование – полезный инструмент на страже Вашего времени.

Пример


Смотрите по этой ссылке webpack-boilerplate

Спасибо, что прочитали.

Комментарии (13)


  1. koroandr
    04.03.2016 10:05

    Спасибо за статью, сам пользуюсь подобным стеком технологий, отсюда два вопроса:

    1. Скажите пожалуйста, а в чем преимущество chai перед встроенными ассертами jasmine? Просто выглядит это все примерно одинаково (с точностью до синтаксиса), а иметь лишнюю зависимость не хочется. Насколько я понял, chai придумывался больше для работы с mocha.
    2. У Вас завелись source map'ы? У меня примерно такая же связка, только typescript вместо babel'я, я перепробовал все опции webpack на тему source map'ов, ни одна из них не привела к адекватному результату.


    1. mrsum
      04.03.2016 10:47

      Лично меня chai подкупил синтаксисом expect. Показалось удобно писать правила через точку. Лаконичнее чтоли, хотя конечно же дело вкуса :)
      Да sourcemap завелись как надо с первого раза, сам был очень приятно удивлен.


      1. koroandr
        04.03.2016 13:46
        +2

        По поводу chai:
        Вот пример одной и той же проверки (если нигде не напутал) на chai и на "чистом" jasmine:

        expect(Instance).to.have.property('render'); //chai
        expect(Instance.render).toBeDefined(); //jasmine

        Как по мне, так разница совсем незначительная. Вот я и подумал, вдруг chai дает какие-то убер-плюшки по сравнению с jasmine.


        1. mrsum
          04.03.2016 13:49

          Решив для себя что пора бы начать писать тесты, внимательно изучил chai-webdriver и подумал что может пригодиться. Альтернатив в Jasmine к сожалению не нашел, хотя наверняка есть.


      1. nightstalker
        05.03.2016 23:36
        +2

        Если уж используете chai, то каков смысл использовать jasmine, а не mocha?


        1. nightstalker
          05.03.2016 23:37
          +2

          Сейчас заметил что Вы еще и mocha подключаете. Вы точно понимаете как это у Вас там работает?


  1. gearbox
    04.03.2016 13:25

    хочу поделиться опытом смешивания гремучей смеси webpack + jasmine + chai + karma

    Истинно гремучая смесь — typescript + webpack в расширениях для firefox. А если речь о портировании на лису расширения хрома — так и вовсе треш и угар.

    А за статью — спасибо!


  1. zapolnoch
    04.03.2016 14:21

    Вы забыли добавить в свою смесь Mocha, QUnit, CucumberJS, Vows, Sinon и немножко CasperJS для запаха.


  1. dshster
    04.03.2016 15:05

    Вот я вчера мучался с e2e тестированием: опять же Webpack (webpack-dev-server), Webdriver, Protractor c фреймворком Jasmine. Всё хорошо, пока не дошло до тестирования вывода результатов запроса с сервера (а бекенда у меня и нет), при unit-тестировании через Jasmine можно заинжектить в ангуляр $httpBackend и перехватывать http запросы выдавая свои данные.
    А вот с Protractor такой фокус не прошел. Последнее что пробовал это https://github.com/kbaltrinic/http-backend-proxy, но не взлетело. Может есть у кого опыт подобного тестирования?


    1. Xergin
      04.03.2016 18:07

      Не уверен насчет совместимости с webpack, но можно использовать https://github.com/seglo/connect-prism для мокинга ответов от бекенда. Если вы знакомы с ruby/rails, то это по сути аналог vcr.


      1. dshster
        11.03.2016 12:18

        Я разобрался, накидал простой пример на основе protractor-http-mock:
        https://gist.github.com/dshster/8a8d1130a326b23e9694


  1. Finom
    05.03.2016 15:26
    +1

    Я очень поверхностно знаком с тестированием поэтому ваш пост у меня вызвал больше вопросов, чем дал ответов.

    mocha — позволяет группировать тесты (describe, it), добавляет такие штуки как beforeEach и пр.
    chai — собственно, библиотека для тестирования: мы делаем X и ожидаем от этого Y
    jasmine — делает и то и то: группирует тесты и описывает ожидания

    Вопросы:

    • Зачем использовать jasmine, если вы всё равно используете mocha и chai?
    • Зачем использовать mocha-reporter если можно воспользоваться jasmine-reporter, если вы всё таки решили использовать jasmine?
    • Объединяя первый и второй вопрос: получается, что jasmine + jasmine-reporter и mocha + chai + mocha-reporter — взаимозаменяемы. Зачем нужна такая смесь?

    Надеюсь, что вопросы имеют смысл.


  1. freakru
    06.03.2016 11:34
    +1

    Современный JavaScript сейчас бурно развивается, поэтому появляется много похожих библиотек, плагинов к ним и плагинов к плагинам. Я бы посоветовал прежде чем писать туториалы, немного разобраться в них.