Привет!
Пару месяцев назад я писал пост, о том как научить 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 файлов
Опыт показывает, что спеки (от англ 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
Первый тест
Для примера, предлагаю рассмотреть нетривиальную с точки зрения 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');
});
});
Запускаем тестирование и наблюдаем за результатом.
В дополнении ко всему, директория tmp/coverage/html-report/ будет содержать отчет о покрытии кода:
Вывод
Тесты, даже в таком формальном виде, избавят нас от обязательств перед собой.
Применив достаточную изобретательность в их декларации, мы можем уберечь себя и коллег от головной боли.
В заключении, представьте то количество времени, которое мы ежедневно тратим на каждую итерацию: "изменил – сохранил – обновил браузер – увидел результат".
Очевидное рядом. Тестирование – полезный инструмент на страже Вашего времени.
Пример
Смотрите по этой ссылке webpack-boilerplate
Спасибо, что прочитали.
Комментарии (13)
gearbox
04.03.2016 13:25хочу поделиться опытом смешивания гремучей смеси webpack + jasmine + chai + karma
Истинно гремучая смесь — typescript + webpack в расширениях для firefox. А если речь о портировании на лису расширения хрома — так и вовсе треш и угар.
А за статью — спасибо!
zapolnoch
04.03.2016 14:21Вы забыли добавить в свою смесь Mocha, QUnit, CucumberJS, Vows, Sinon и немножко CasperJS для запаха.
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, но не взлетело. Может есть у кого опыт подобного тестирования?Xergin
04.03.2016 18:07Не уверен насчет совместимости с webpack, но можно использовать https://github.com/seglo/connect-prism для мокинга ответов от бекенда. Если вы знакомы с ruby/rails, то это по сути аналог vcr.
dshster
11.03.2016 12:18Я разобрался, накидал простой пример на основе protractor-http-mock:
https://gist.github.com/dshster/8a8d1130a326b23e9694
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 — взаимозаменяемы. Зачем нужна такая смесь?
Надеюсь, что вопросы имеют смысл.
freakru
06.03.2016 11:34+1Современный JavaScript сейчас бурно развивается, поэтому появляется много похожих библиотек, плагинов к ним и плагинов к плагинам. Я бы посоветовал прежде чем писать туториалы, немного разобраться в них.
koroandr
Спасибо за статью, сам пользуюсь подобным стеком технологий, отсюда два вопроса:
mrsum
Лично меня chai подкупил синтаксисом expect. Показалось удобно писать правила через точку. Лаконичнее чтоли, хотя конечно же дело вкуса :)
Да sourcemap завелись как надо с первого раза, сам был очень приятно удивлен.
koroandr
По поводу chai:
Вот пример одной и той же проверки (если нигде не напутал) на chai и на "чистом" jasmine:
Как по мне, так разница совсем незначительная. Вот я и подумал, вдруг chai дает какие-то убер-плюшки по сравнению с jasmine.
mrsum
Решив для себя что пора бы начать писать тесты, внимательно изучил chai-webdriver и подумал что может пригодиться. Альтернатив в Jasmine к сожалению не нашел, хотя наверняка есть.
nightstalker
Если уж используете chai, то каков смысл использовать jasmine, а не mocha?
nightstalker
Сейчас заметил что Вы еще и mocha подключаете. Вы точно понимаете как это у Вас там работает?