Привет!
Пару месяцев назад я писал пост, о том как научить 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
Спасибо, что прочитали.
Автор: Rambler&Co