JS testing. Karmic Webpack
Hello!
A couple of months ago I wrote a post on how to teach webpack for spa .
From that moment, the tool stepped forward and overgrown with an additional number of plug-ins, as well as examples of configurations.
In this article I want to share the experience of mixing the explosive mixture webpack + jasmine + chai + karma.
The best, in my opinion, book about automated testing by Christian Johansen - Test-Driven JavaScript Development - identifies problems that the developer encounters when writing code without tests:
- The code is written, but the behavior is not available in the browser (example .bind () and IE 8);
- Implementation is changed, but the combination of components leads to erroneous or not working functionality;
- The new code is written, you need to take care of the behavior with the old interfaces.
Based on experience, I will say.
Programmers who choose the samurai path of TDD (Test-driven development) spend a lot of time covering code with tests. As a result, they benefit from testing and catching bugs.
Glossary
- Webpack - a modular asset collector;
- Karma - test-runner for JavaScript;
- Jasmine - a tool for defining tests in the style of BDD;
- Chai - a library for checking conditions, expect , assert , should;
Package Installation
To begin with, I’ll give a list of packages that are additionally installed in the project. We will use npm for this .
#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
Move on.
Environment setting
After installing additional packages, configure karma. To do this, create the file karma.conf.js in the root of the project
touch karma.conf.js
With the following content:
// 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
}
});
};
Configuring 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]'
}
}
],
},
};
};
We are ready to write and run the first test.
Definition of spec files
Experience shows that specs (from the English spec - specification) are more convenient to store in the same folders as the tested components. Although, of course, you yourself build the architecture of your application. In the example below, you will find the only test example for the trial article, which is located in the tests directory of the boilerplate module .
Such directory naming gives a positive response from new developers who want to get acquainted with the functionality of a module or component.
TL; DR opening the project, we see the folder with specifications, located in first place due to string sorting.
Launch
Nothing new here.
To start, I use the built-in npm functionality of the scripts section.
Exactly the same as for the dev-server and the "battle" build functionality.
In package.json, we declare the following commands:
"scripts": {
...
"test:single": "rm -rf tmp/ && karma start karma.conf.js --single-run --browsers PhantomJS",
"test:watch": "karma start karma.conf.js --browsers PhantomJS"
...
}
To run the tests in the "update on change" mode, at the root of the project we type the command:
npm run test:watch
For a one-time launch:
npm run test:single
First test
For example, I propose to consider a task that is not trivial in terms of unit testing. Processing the result of Backbone.View.
It's okay if the first test looks like a formality.
Consider the View code:
// 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());
}
});
It is expected that when you create an instance of View, the render () function will be called. The result of which will be html - declared in the hello.jade template
Example of a formal test covering functionality:
// boilerplate.spec.js 'use strict'; const $ = require('jquery'); const Module = require('_modules/boilerplate'); describe('App.modules.boilerplate', function() { // подготовим переменные для использования let $el = $('
', { 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'); }); });
We start testing and observe the result.
In addition to everything, the tmp / coverage / html-report / directory will contain a code coverage report:
Conclusion
Tests, even in such a formal form, will relieve us of our obligations to ourselves.
Using sufficient ingenuity in their declaration, we can protect ourselves and our colleagues from headaches.
In conclusion, imagine the amount of time that we spend daily on each iteration: "changed - saved - updated the browser - saw the result."
The obvious is near. Testing is a useful tool to guard your time.
Example
See this link webpack-boilerplate
Thank you for reading.