Тестирование JS. Кармический Webpack

в 20:51, , рубрики: chai, jasmine, javascript, karma, tdd, testing, webpack, Веб-разработка, Тестирование веб-сервисов

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

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

Автор: Rambler&Co

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js