こんにちは、ほそ道です。
今回から連作でテストコーディングの進め方・考え方を取り上げます。
フロントエンドテストはブラウザ・DOMが絡んできたりしてとかく複雑で厄介なものです。
あとGulpとかのビルドツールと連携しようとしたり、WebpackとかでテストコードもES6で描きたいなーとかのトライもいっぺんに含めようとすると軽くカオスな状態になります。
最近はReactのTestUtilだったりFluxテストでJestをお勧めしてたりとモダンな奴らもチラホラ。
というわけで知ることテンコ盛りなので今回は第一歩としてテストフレームワークJasmineを使ってブラウザレスでしっかりと単体テストを行い基盤を固めたいと思います。
前提:モジュール間の依存関係をどう受け止めるか
今回の主題は「モジュール間の依存関係をどう受け止めるか」に設定しました。
テストしやすいコードを書くには、TDDで進めるためには、とかもあるんですが
今回は依存関係に影響を受けないための道具の使い方をチェックしていきます。
なんで依存したくないのか
例えば、あるオブジェクトOのメソッドmの引数にaを与えるとrが帰ってくるべし。みたいなテストはシンプルだし想像つきやすいと思います。
ところがあるオブジェクトOが別モジュールのオブジェクトPのメソッドを呼び出していたり、オブジェクトPを生成していたりすると
話は一気にややこしくなってきます。
「いや、オブジェクトPを実装・参照すればいいじゃない」。それも一理あります。
ではもしも、オブジェクトPは別担当者のJさんが作っていたら、Jさんブランチがマージされるまで実装コードは直接もらわないとわかりません。
Jさんは実装コードの中のロジックを書き換えるかもしれません。またコピペしなおすのかぁ。。とか。
サーバーサイドJSでテスト実行するごとにDBのレコードが増えたら嫌だなぁ、とかもありますし。
うーん、オブジェクトPに依存したオブジェクトOのテストは単体で実行できる方がいいなあ。
モジュールOのテストモジュールはあくまでも対象モジュールであるOのテストができればいいなぁ。
というわけでそんな悩みを解決していきます!
Jasmineを実行してみる
テスト実行環境を整える
それではテストフレームワークであるJasmineを実行するところからやってみましょう。
まずは空のプロジェクトディレクトリに入ってコンソールで必要なライブラリを取り込みます。
npm init
npm i -D jasmine jasmine-spec-reporter rewire
次に実行コマンドを整えます。次のコマンドを打ちます。
jasmine init
spec
ディレクトリができたと思います。
併せて生成されたspec/support/jasmine.json
を見ると下記のようになっています。
{
"spec_dir": "spec",
"spec_files": [
"**/*[sS]pec.js"
],
"helpers": [
"helpers/**/*.js"
]
}
spec
ディレクトリにある*[sS]pec.js
ファイルが実行されそうですね。
早速作ってみることにします。足し算
describe('テストことはじめ', function() {
function add(x, y) { return x + y; }
it('足し算テスト', function() {
expect(add(1,2)).toBe(3);
});
});
Jasmineのメソッド群を簡単に解説いれますと
-
describe
: 第一引数がテストすべき内容の大枠の説明文字列、第二引数がテストコードの入れ物になります。describe
は入れ子にできます。 -
it
: 第一引数がテストすべき内容の詳細説明。第二引数がテストコードになります。 -
expect
: 期待値を表すExpectation
オブジェクトを生成します。テストするため引数の内容を格納します。 -
Expectation.toBe
:===
チェックを行います。他にもtoXXで様々なチェックができます。
それでは下記のコマンドを叩いてみましょう
jasmine
なんか味気ない画面でテスト成功が伝えられたかと思います。
うーん。。
ほそ道的にはなんか物足りない!テスト通したカタルシスが足りないっす!!!
ということで次は結果レポートの表示を変えてみたいと思います。
結果レポートの表示内容を変更
プロジェクトルートにjasmine-runner.js
というファイルを作ります。内容は公式から引っ張ってきた下記となります。
var Jasmine = require('jasmine');
var SpecReporter = require('jasmine-spec-reporter');
var noop = function() {};
var jrunner = new Jasmine();
jrunner.configureDefaultReporter({print: noop});
jasmine.getEnv().addReporter(new SpecReporter());
jrunner.loadConfigFile();
jrunner.execute();
ここは本筋ではないので解説省きます。というかほそ道も実装やオプションを追いかけてませんので。。
それでは実行してみましょう。
node jasmine-runner.js
うん、こっちの方が好みです。とりあえずコレでいっておきましょう。
npm test
最後にひとネタ。
より楽チン・よりスタンダードにテストコマンドを実行できるようにしておきましょう。
package.json
の一行を書き換えます。
下記の部分を
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
以下のように変更します。
"scripts": {
"test": "node jasmine-runner.js"
},
これで以降は
npm test
でテストが実行できるようになります。
実践的なテストケース
非依存なモジュールテスト
それでは簡単なケースからやっていきましょう。
さっき作ったspec/testSpec.js
は削除して構いません。
src/Food.js
とspec/FoodSpec.js
の2つのファイルを作ります。
プロジェクトの状態は下記のようになっていると思います。
.
|-- jasmine-runner.js
|-- node_modules
|-- package.json
|-- spec
| |-- FoodSpec.js
| `-- support
| `-- jasmine.json
`-- src
`-- Food.js
それではFood.jsからみていきます。
これはweightというプロパティを持ったコンストラクタで、食べ物の名前によってweightが異なります。
他のどのモジュールも参照していません。
var Food = function(name) {
this.weight = 0;
switch(name) {
case 'chikuwa':
this.weight = 1;
break;
case 'fish':
this.weight = 2;
break;
case 'beef':
this.weight = 3;
}
};
module.exports = Food;
それではテストコードを書きましょう
var Food = require('../src/Food');
describe('Food constructor', function() {
it('ちくわのweightは1であるべし', function() {
expect(new Food('chikuwa').weight).toBe(1);
});
it('魚のweightは2であるべし', function() {
expect(new Food('fish').weight).toBe(2);
});
it('ビーフのweightは3であるべし', function() {
expect(new Food('beef').weight).toBe(3);
});
it('未設定なFoodのweightは0であるべし', function() {
expect(new Food('Daikon').weight).toBe(0);
});
});
引数を変えながら期待値を確かめていきます。
それでは実行してみましょう。
いい感じですね。
一応コカしてもおきましょう。
ビーフをポークに変えてみます。3つ目のテストのbeefをporkに変えてみましょう。
エラーが出てくれました。
ここまでは簡単だと思います。
軽依存なモジュールテスト
さて、いよいよ本題です。
依存関係が現れた時、どのようにモジュール単体でテストしていくのかをみていきましょう。
今度はさっきの食べ物を食べる主体者としてネコを登場させてみましょう。
src/Cat.js
とspec/CatSpec.js
を作りましょう。
プロジェクトの状態は下記のようになります。
.
├── jasmine-runner.js
├── node_modules
├── package.json
├── spec
│ ├── CatSpec.js
│ ├── FoodSpec.js
│ └── support
│ └── jasmine.json
└── src
├── Cat.js
└── Food.js
それではコードをみていきます。
var Cat = function (name) {
this.name = name ? name : 'Tama';
this.weight = 5;
};
Cat.prototype.eat = function (food) {
this.weight += food.weight;
};
module.exports = Cat;
デフォルトの名前はTamaだよ、とか入ってますが注目すべきところはeatメソッドです。
引数にFoodをとって、食った分だけネコの体重が増加するようになっています。
さて、どのように単体でテストできるよう解決すべきか。
ちと考えてみましょう。
Hint:ここでCatを軽依存なモジュールと定義しているのはFoodモジュールをrequireしていないことに起因しています。
本当のFoodは必要か…?
それではspec/CatSpec.js
をみていきましょう
var Cat = require('../src/Cat');
describe('Cat', function () {
describe('.constructor()', function() {
it('デフォルト名前はTamaになるべし', function () {
expect(new Cat().name).toBe('Tama');
});
it('名前を指定すればその名が設定されるべし', function () {
expect(new Cat('Mike').name).toBe('Mike');
});
it('最初の体重は5となるべし', function () {
expect(new Cat().weight).toBe(5);
});
});
describe('.eat()', function() {
it('食べ物のweighだけネコの体重が増加すべし', function () {
var c1 = new Cat();
var c2 = new Cat();
c1.eat({weight: 8});
expect(c1.weight - c2.weight).toBe(8);
});
});
});
一番下のeatメソッドのテストがポイントです。
要はweihtプロパティに数値を持ったオブジェクトを引数として渡してあげればいいわけですね。
なんだ、大したことないねぇ。なんて思いながら一応テストを実行しておきましょう。あ、ポークの件は元に戻しておきましょう
重依存なモジュールテスト
それでは今回の本丸です。
モジュール内で他モジュールをrequireして、メソッドコールしていた場合です。
ここでネコのことならなんでもお任せ、CatServiceモジュールを登場させます。
このモジュールは外部モジュールからのコールでネコに関する依頼をいろいろ承ります。
src/CatService.js
とspec/CatServiceSpec.js
を作りましょう。
プロジェクトの状態は下記のようになります。
.
├── jasmine-runner.js
├── node_modules
├── package.json
├── spec
│ ├── CatServiceSpec.js
│ ├── CatSpec.js
│ ├── FoodSpec.js
│ └── support
│ └── jasmine.json
└── src
├── CatService.js
├── Cat.js
└── Food.js
それではsrc/CatService.js
をみていきましょう。
var Cat = require('./Cat');
var Food = require('./Food');
function CatService() {
this.cats = {};
}
// お泊まりサービスをやっています
CatService.prototype.checkin = function (cat) {
this.cats[cat.name] = cat;
};
CatService.prototype.checkout = function (name) {
var cat = this.cats[name];
delete this.cats[name];
return cat;
};
// 依頼された猫に指定の餌をあげるサービスです
CatService.prototype.feed = function (name, foodName) {
this.cats[name].eat(new Food(foodName));
};
// 猫を産んでお客様に差し上げるサービスです
CatService.prototype.yield = function (name) {
return new Cat(name);
};
module.exports = CatService;
はい、バリバリ依存(require)しています。
特にfeedメソッド、yieldメソッドに関してはnew
までしちゃってるのでCatSpec.jsの時のような誤魔化しは効きません。
いやさ、その難題、誤魔化しきってみようじゃないっすか!
あと補足でメモ入れておきます。
対象モジュール以外のテストはしなくて良い。例えばCatのデフォルト名がTamaだとかeatすると体重が増えるとかはここでテストせずにCatSpecに任せれば良い。
rewireでモック注入
はい、ここで最初にnpmインストールしておいたrewire
が登場します。
rewire
はなにが嬉しいんでしょう。
モジュールが参照しているオブジェクトにモックを注入できる。
すなわちCatServiceが参照するCatやFoodを必要なインターフェースだけ持った簡易なCatモック、Foodモックに差し替えることができます。 うぉぉ
使い方はいたって簡単。レシピを下記に書き添えます。
rewire
をrequireします- テストモジュールを
require
ではなくrewire(Module)
で取り込みますModule.__set__('差し替えたいモジュール名', 差し替えたいモジュール)
を実行します
以上です。簡単そうですね!
jasmine + rewireであくまで単体モジュールテスト
それではspec/CatServiceSpec.js
の実装を見てみましょう。
var rewire = require('rewire'),
CatService = rewire('../src/CatService');
describe('CatService', function () {
var service = new CatService(),
// ネコモック
Cat = function (name) {
this.name = name;
this.weight = 5;
this.eat = function(food){};
},
// 食い物モック
Food = function(weight) {
this.weight = weight;
};
// モック注入!
CatService.__set__('Cat', Cat);
CatService.__set__('Food', Food);
describe('.checkin()', function () {
beforeEach(function() {
// 検査ごとにserviceを初期化
service = new CatService();
});
it('別名のネコを2匹泊めたらcatsには2匹いるべし', function () {
service.checkin(new Cat('Tama'));
service.checkin(new Cat('Mike'));
expect(Object.keys(service.cats).length).toBe(2);
});
it('同名のネコのお泊まりは残念ながら受け付けぬべし', function () {
service.checkin(new Cat('Mike'));
service.checkin(new Cat('Mike'));
expect(Object.keys(service.cats).length).toBe(1);
});
});
describe('.checkout()', function() {
// あえてbeforeEachせずに直上の状態を引き継いでみる
it('指定したネコを返却すべし', function() {
var 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()', function () {
beforeEach(function() {
service = new CatService();
});
it('Cat.eatメソッドが呼ばれるべし', function () {
var mike = new Cat('Mike');
spyOn(mike, 'eat');
service.checkin(mike);
service.feed('Mike', new Food(3));
expect(mike.eat).toHaveBeenCalled();
});
});
describe('.yield()', function () {
it('新しいネコを生成すべし', function () {
expect(service.yield('Mike') instanceof Cat).toBeTruthy();
});
});
});
上の方でCatモックとFoodモックを生成しています。
そして__set__
メソッドでCatServiceにCatモックとFoodモックを注入しています。
あとはさも、自分が書いたインターフェースを元にCatとFoodがあるかのようにCatServiceをテストするコードを書けば良いのです。
一応、新しく登場したjasmineのメソッドも取り上げておきます。
-
beforeEach
:describe
内の各it実行前に共通の初期化処理を行います。呼ばなかった場合、オブジェクトの状態は次に持ち越されます。 -
spyOn
: 第一引数にモジュール、第二引数にそのモジュールのメソッド名で指定します。指定したメソッドを追跡します。 -
Expectation.toHaveBeenCalled
:spyOn
したメソッドが呼ばれたかどうかをチェックします。 -
Expectation.toBeTruthy
: trueであるかどうかをチェックします。
さて、テスト実行してみたいところですがこのままだとCatやFoodが普通に使われちゃってるかもわかりません。
CatとFoodモジュールのコードは大胆に全てコメントアウトしちゃいましょう!
さて、実行。
はい、CatとFoodのテストはガッツリ失敗してますがCatServiceのテストはオールグリーンです!
これはCatServiceの単体モジュールテストがうまくいってる証です。
Jasmineのメソッドをおさらい
最後に今回登場したJasmineのメソッドを見やすいように改めて全部まとめておきます。
メソッド | 内容 |
---|---|
describe |
第一引数がテストすべき内容の大枠の説明文字列、第二引数がテストコードの入れ物になります。describe は入れ子にできます。 |
it |
第一引数がテストすべき内容の詳細説明。第二引数がテストコードになります。 |
beforeEach |
describe 内の各it実行前に共通の初期化処理を行います。呼ばなかった場合、オブジェクトの状態は次に持ち越されます。 |
spyOn |
第一引数にモジュール、第二引数にそのモジュールのメソッド名で指定します。指定したメソッドを追跡します。 |
expect |
期待値を表すExpectation オブジェクトを生成します。テストするため引数の内容を格納します。 |
Expectation.toBe |
=== チェックを行います。他にもtoXXで様々なチェックができます。 |
Expectation.toBeTruthy |
trueであるかどうかをチェックします。 |
Expectation.toHaveBeenCalled |
spyOn したメソッドが呼ばれたかどうかをチェックします。 |
今回は以上です。
次回はブラウザやDOMでどうやってテストするんだーというあたりをまとめていきたいなと思います。