Тестування JS. Кармічний Webpack

image

Привіт!

Пару місяців тому я писав пост про те, як навчити webpack для spa.
З того моменту інструмент ступив уперед і обріс додатковою кількістю плагінів, а так само прикладами конфігурацій.

В цій статті хочу поділитися досвідом змішування гримучої суміші webpack + jasmine + chai + karma.

У кращій, на мою думку, книзі про автоматизоване тестування Christian Johansen — Test-Driven JavaScript Development – визначені проблеми, з якими розробник стикається при написанні коду без тестів:

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

Спираючись на досвід, скажу.
Програмісти, які обрали шлях самурая TDD (Test-driven development ), витрачають багато часу на покриття коду тестами. У результаті залишаються у виграші на етапі тестування та відловлювання багів.

Словник
Webpack — модульний складальник ассетов;
Facebook — 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/ && start karma 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 and el $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

Дякую, що прочитали.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.