こんにちは、ほそ道です。
今回はビルドツール・モジュールローダ・altJS(JSコンパイラ)を組み合わせてさらにテストコードのおくのほそ道に入り込んでいきます。
今回Gulp + Webpack + Karma + Jasmine + ES6を使用しますが、
GruntだったりBrowserifyだったりMochaだったりCoffeescriptだったりに置き換えても基本的な考え方は近しい感じになるかと思います。
あとはここにSPAフレームワークとかを組み合わせればいよいよモダンなテストコードになっていきますね。
今回解決すること・設計方針
- Webpackとテストコードを組み合わせる。なんかバンドルしたりごにょごにょやってるどこにテストを挟むのか?を解決する。
- ソースコードもテストコードもES6で書く。babelないしはaltJSのコンパイルごにょごにょやってるどこにテストを挟むのか?を解決する。
- Gulpも使ってるんだけど誰に何の仕事をさせてどういう順番で動かせばテストになるのか・・・を解決する
今回の解決方法としてはアプリケーションビルドとは別枠でテストを走らせます
それぞれのテストモジュールごとにloader付きのwebpackビルドが走りテストを行わせます
GulpはWebpackのキックだけでビルド周りの情報は薄めさせる。もっと大きな前処理/後処理(ファイル構成いじったり)とかはGulpに任せる。
では、やっていきましょう。
まずは被テストコードを用意
まずはサクッと動くコードを用意しましょう。
JSコードはモジュールテストの回で扱ったネコサービスをES6に書き直します。
ここの内容が良く解らなければwebpack入門を併せてお読みください。
ネコサービスとテストの初期実装はこちらです。
ES6についてはまとめてませんが。。
プロジェクト構成は下記となります。
├── node_modules
├── src
│ ├── Cat.js
│ ├── CatService.js
│ ├── Food.js
│ └── entry.js
├── package.json
└── webpack.conf.js
まずはwebpackとloaderのインストールから
npm i -D webpack babel-loader
続いてwebpack.conf.js
です。
module.exports = {
entry: { app: ["./src/entry.js"] },
output: { filename: "build/[name].js" },
devtool: "#source-map",
module: { loaders: [ {test: /\.js$/, loader: "babel"} ] }
};
では被テストコードを用意します。
export default class Food {
constructor(name) {
this.name = name;
this.weight = 0;
switch(name) {
case 'chikuwa':
this.weight = 1;
break;
case 'fish':
this.weight = 2;
break;
case 'beef':
this.weight = 3;
}
}
}
export default class Cat {
constructor(name) {
this.name = name ? name : 'Tama';
this.weight = 5;
}
eat(food) {
this.weight += food.weight;
}
}
import Cat from './Cat'
import Food from './Food'
export default class catService {
constructor() {
this.cats = {}
}
// お泊まりサービスをやっています
checkin(cat) {
this.cats[cat.name] = cat;
}
checkout(name) {
const cat = this.cats[name];
delete this.cats[name];
return cat;
}
// 依頼された猫に指定の餌をあげるサービスです
feed(name, foodName) {
this.cats[name].eat(new Food(foodName));
}
// 猫を産んでお客様に差し上げるサービスです
newCat(name) {
return new Cat(name);
}
}
これでコードが揃いました。
webpackビルドすればちゃんと動くことが確認できると思います。
webpack --config webpack.conf.js
さあテスト!
上で作成したコード群をテストしていきましょう。
ここでの内容が良く解らない方は
karmaを使ったUIテストを併せてお読みいただければ幸いでございます。
最終的なプロジェクト構成は下記となります。
├── node_modules
├── src
│ ├── Cat.js
│ ├── CatService.js
│ ├── Food.js
│ └── entry.js
├── spec
│ ├── CatServiceSpec.js
│ ├── CatSpec.js
│ └── FoodSpec.js
├── package.json
├── karma.conf.js
└── webpack.conf.js
パッケージインストール
パッケージを追加インストールします。
# karmaの回で取り上げたパッケージ
npm i -D karma karma-jasmine karma-chrome-launcher karma-firefox-launcher karma-spec-reporter
# 今回初登場のパッケージ
npm i -D karma-webpack babel-plugin-rewire babel-runtime
今回、キモとなるパッケージは初登場のkarma-webpackです。これがkarmaに独自に行うwebpack設定を有効にし、karma startコマンドにwebpack連動させる役割を果たします。
karma.conf.js
では一番大事なkarma.conf.jsです。
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine'],
files: ['spec/*.js'],
exclude: [],
preprocessors: {'spec/*.js': ['webpack']},
reporters: ['spec'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome', 'Firefox'],
singleRun: false,
webpack: {
module: { loaders: [
{test: /\.js$/, exclude: /node_modules/, loader: 'babel?plugins=rewire&optional=runtime'}
]}
},
plugins: [
'karma-webpack', 'karma-jasmine',
'karma-chrome-launcher', 'karma-firefox-launcher', 'karma-spec-reporter'
]
});
};
- 新しく出てきた要素のピックアップ
要素 | やってること |
---|---|
files ['spec/*.js'] |
karma回ではsrcも呼び出して相互参照できるようにしていましたが今回はspecのみとします。 |
preprocessors {'spec/*.js': ['webpack']} |
テストディレクトリspecのそれぞれのファイルに対してwebpackをカマします。 |
webpack: {...} |
ルートに作ったwebpack.conf.jsとは別にテストのためのwebpack設定を記述します。entryやoutput要素は入れません。 |
webpack.module.loaders:[...] |
ローダー設定を記述しますが今回注目はbabel?plugins=rewire&optional=runtime の部分です。これはbabel・webpack環境で使用できるrewireを使えるようにコンパイルに組み込んでいます。今回環境ではJasmine回で紹介したrewireは使えません。 |
plugins:[...] |
karmaで使用するプラグインを指定します。karma-webpackを使う場合はこの指定が必要になります。 |
テストコード
Food.jsとCat.jsは特筆すべき点ないと思います。
import Food from '../src/Food.js'
describe('Food constructor', () => {
it('ちくわのweightは1であるべし', () => {
expect(new Food('chikuwa').weight).toBe(1);
});
it('魚のweightは2であるべし', () => {
expect(new Food('fish').weight).toBe(2);
});
it('ビーフのweightは3であるべし', () => {
expect(new Food('beef').weight).toBe(3);
});
it('未設定なFoodのweightは0であるべし', () => {
expect(new Food('Daikon').weight).toBe(0);
});
});
import Cat from '../src/Cat.js'
describe('Cat', function () {
describe('.constructor()', () => {
it('デフォルト名前はTamaになるべし', () => {
expect(new Cat().name).toBe('Tama');
});
it('名前を指定すればその名が設定されるべし', () => {
expect(new Cat('Mike').name).toBe('Mike');
});
it('最初の体重は5となるべし', () => {
expect(new Cat().weight).toBe(5);
});
});
describe('.eat()', function() {
it('食べ物のweighだけネコの体重が増加すべし', () => {
var c1 = new Cat();
var c2 = new Cat();
c1.eat({weight: 8});
expect(c1.weight - c2.weight).toBe(8);
});
});
});
CatServiceはrewire周りを下記のように変更しています。これはwebpack・babelに対応できるようにするためbabel-plugin-rewire
パッケージを使うようにしたためです。
-
require(rewire)
的なことをしない。 - モックを設定する
__set__
メソッドを__Rewire__
メソッドに変更する。
import CatService from '../src/CatService.js'
describe('CatService', () => {
let service = new CatService();
// ネコモック
class Cat {
constructor(name) {
this.name = name;
this.weight = 5;
this.eat = food => {}
}
}
// 食い物モック
class Food {
constructor(weight) {
this.weight = weight;
}
}
// モック注入!
CatService.__Rewire__('Cat', Cat);
CatService.__Rewire__('Food', Food);
describe('.checkin()', () => {
beforeEach(() => {
// 検査ごとにserviceを初期化
service = new CatService();
});
it('別名のネコを2匹泊めたらcatsには2匹いるべし', () => {
service.checkin(new Cat('Tama'));
service.checkin(new Cat('Mike'));
expect(Object.keys(service.cats).length).toBe(2);
});
it('同名のネコのお泊まりは残念ながら受け付けぬべし', () => {
service.checkin(new Cat('Mike'));
service.checkin(new Cat('Mike'));
expect(Object.keys(service.cats).length).toBe(1);
});
});
describe('.checkout()', () => {
// あえてbeforeEachせずに直上の状態を引き継いでみる
it('指定したネコを返却すべし', () => {
let returnedCat;
service.checkin(new Cat('Tama'));
returnedCat = service.checkout('Tama');
// Mikeだけが残ってるはず
expect(Object.keys(service.cats).length).toBe(1);
expect(Object.keys(service.cats)[0]).toBe('Mike');
// Tamaが返却されるはず
expect(returnedCat.name).toBe('Tama');
})
});
describe('.feed()', () => {
beforeEach(() => {
service = new CatService();
});
it('Cat.eatメソッドが呼ばれるべし', () => {
const mike = new Cat('Mike');
spyOn(mike, 'eat');
service.checkin(mike);
service.feed('Mike', new Food(3));
expect(mike.eat).toHaveBeenCalled();
});
});
describe('.newCat()', () => {
it('新しいネコを生成すべし', () => {
expect(service.newCat('Mike') instanceof Cat).toBeTruthy();
});
});
});
テスト実行
最後にpackage.jsonにテスト実行コードを登録しましょう
{
…
"scripts": {
"test": "karma start karma.conf.js"
},
…
}
さあ、テスト実行です。
npm run
※うまく動かない時
babel-plugin-rewireとbabel-loaderはそれぞれがbabel-coreを参照しておりコンフリクトを起こす場合があります。
ほそ道の環境では下記を叩いて一括で入れ直したらうまく動くようになりました。
npm i -D babel-plugin-rewire babel-loader babel-core
コンソールの内容から**Spec.jsごとにwebpackがモジュールを取り込みビルドしていることがわかります。
Gulpから実行する
最後にGulpも絡めておきましょう。やる事はwebpackビルドとkarmaテストの実行です。
とは言っても、ここまででベースはできているので叩くところだけ任せれば良いです。
以前のビルドのクリアなどの前処理、ミニファイなどの後処理はこっちでやったほうがいいと思います。
ではさらっとインストールから。
npm i -D gulp gulp-webpack-build
ルートにgulpfileを作りましょう。
今回はソリッドな構成にしましたし、こいつに固有の設定を色々持たせないほうがシンプルでいられると思います。
var gulp = require('gulp'),
webpack = require('gulp-webpack-build'),
karma = require('karma').server,
WEBPACK_CONFIG = __dirname + '/webpack.conf.js',
KARMA_CONFIG = __dirname + '/karma.conf.js';
gulp.task('webpack-build', function () {
return gulp.src(WEBPACK_CONFIG)
.pipe(webpack.run())
.pipe(gulp.dest(''));
});
gulp.task('karma-test', function () {
karma.start({configFile: KARMA_CONFIG});
});
実行コマンドは以下です。
# ビルド実行
gulp webpack-build
# テスト実行
gulp karma-test
まとめ
今回新しく登場したパッケージのまとめです。
パッケージ名 | 概要 |
---|---|
karma-webpack | karmaの実行に併せてテストコードに必要なモジュールのバンドル化とコードコンパイルをwebpackを使って実行できる |
babel-plugin-rewire | babelコンパイル環境下でrewireを使えるようにする |
babel-runtime | ES6構文のPolyfillのために入れました。今回の場合はbabel-plugin-rewireでObject.assignメソッドを使えるようにするため |
gulp-webpack-build | gulp経由からwebpackを実行します。 |
さて、今回でだいぶ実践的な環境に近づいてきました。テストコードの世界、奥深いですねぇ。
またSPAフレームワークや遷移が絡むテストについても随時ピックアップしていきたいと思います。
今回は以上です。