前回はjQueryプロジェクトからユニットテストの手法を覗きましたが、
その前にユニットテスト自体の話をするべきでしたね。
頭にはてなマークが浮かんでいる人の光景が目に浮かびます…。
今回はしっかり書こうと思います。
皆さん、日頃ユニットテスト書いてますか?
- ユニットテストを書くほど暇じゃない。
- 小規模なプロジェクトだとそれほど必要性を感じない。
- ユニットテスト書くとどうゆう恩恵を受けれるの?
- てゆうかそもそもユニットテストってなに。
こんな声が聞こえてきそうですが、
人それぞれ思うところはあると思います。
個人的にはユニットテストはメリット・デメリットを理解していれば素晴らしいものですし、
時間に余裕があれば書くに越したことはないと思います。
強制するものでもないので、気軽な気持ちで読んでみてください。
全ては効率的に信頼性のあるコードを書くために。
(キャーキャー言われるフロントエンドエンジニアを目指すために。)
ユニットテストとは
普段ユニットテストを書いている人はテストの重要性を理解しているかと思いますが、
普段あまり書いていない皆さんにはその目的がわからないんじゃないでしょうか。
そもそもユニットテストってなによ、なんなのよ。
Webアプリケーション開発においては、単体テストで関数やクラスメソッドをテストし、
結合テストでWebブラウザからの操作をテストするのが一般的です。
前者の単体テストをユニットテストと呼びます。ユニットテストは関数やクラスメソッドを最小処理単位として扱い、引数を処理した「結果」と、想定される「期待値」の2つの関係を比較するものです。
ユニットテストというのは、関数やオブジェクトのメソッドなどの比較的小さいモジュール単位で期待値通りの結果になるかテストする、ということなんですね。
じゃあ、ユニットテストで得られる恩恵はなんだろう?
ユニットテストのメリット
モジュールが結合される前の実装段階でテストが実施されるため、
問題の原因の特定や修正が容易になります。
開発全体のバグ修正コストを下げる効果が高いです。
コードの内容をよく理解している開発者によって、
コード作成と同時にテストケースが作成されるため、妥当性の高いテストケースを資産として残すことができるので、
後から発生する拡張開発や機能改修時にも再利用できます。
資産として残すという点が重要です。
また、テストを意識してコードを書くことでモジュールの疎結合化を促し、
その結果テストがし易い・リファクタリングをしやすくなるという一面も。(これは人によりけり)
個人的にユニットテストを導入することで得られるメリットは大きいと考えます。
良いことずくめですね。
ユニットテストのデメリット
ユニットテストってイケてて最高にクールなやつだぜ!ってわけじゃありません。
当然デメリットもあります。
まず、開発者にかかるコストでしょう。テストを書くのはもちろん時間や労力がかかります。
納期に追われて時間のない中でテストも書くとなると相当大変ですし、テストも不完全なものにもなりやすいです。
開発スピードを取るか、正確性をとるかという点はトレードオフです。
また、コストがかかるほかに、ある程度言語に対する知識やテストに対しての知識が必要になるので、導入のハードルが高いです。
しかしこのデメリットを少なくするためにも、後述するテスティングフレームワークというものがあるわけです。
ユニットテストの為のツール(テスティングフレームワーク)
今回も前回の記事で紹介した QUnit を使います。
他にも Jasmine, mocha, chai などBDD(振舞駆動開発)の様に書けるフレームワークも存在します。
RSpecを書いてる人とかはすんなり導入しやすいですね。
TDDをしたい人には JSTestDriver なども。
QUnitを選ぶ理由
- jQuery だけでなく Javascript 全般のテストが可能
- jQuery プロジェクトチームが開発しているだけあって、実績は十分
- しっかりフレームワーク自体のサポートがされている
- APIがシンプルなので学習コストが低い点
- プラグインが豊富
などが挙げられます。
他にも CI や PhantomJS などと連携したり使い方は無限大です。
もちろん、 QUnit じゃなきゃいけない理由はありません。
ツールに関してはいろいろ試してみてもいいのかなと思っています。
ユニットテストを書く
では QUnit を使ってユニットテストを書いてみましょう。
まず、テスト結果を表示するためのHTMLを用意します。
QUnit を使用する際に必要になるのが、qunit-1.16.0.css
とqunit-1.16.0.js
です。
これは直接ファイルを ダウンロード することもできますが、jQueryのCDNで配信されていますので、手っ取り早くテストを書きたい場合は下記の様にCDNから読み込むのがオススメです。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>QUnit Example</title>
<!-- QUnitのCSS -->
<link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.16.0.css">
</head>
<body>
<!-- テスト結果が挿入されるコンテナ -->
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<!-- QUnit本体 -->
<script src="http://code.jquery.com/qunit/qunit-1.16.0.js"></script>
<!-- テスト対象のJSファイル -->
<script src="tests.js"></script>
</body>
</html>
次にテストを実行するJSファイルを用意します。
QUnit.test( "a basic test example", function( assert ) {
var value = 'hello';
assert.equal( value, "hello", "We expect value to be hello" );
});
QUnit 名前空間に全てが格納されています。
主に使うのはtestメソッドです。
QUnit.test( name, callback )
testメソッドでは第1引数にテストタイトルを設定し、第2引数にテストを走らせた時に実行されるコールバック関数を設定します。
そのコールバック関数の引数(上記ではassert)から各テスト用のメソッドにアクセスするかたちになります。
アサーション
値を比較して出力します。基本はこれです。
本家のCookbookでも基本のアサーションメソッドはこれで決まりだと書かれています。
ok( truthy [, message ] )
// ok(): trueかどうか
QUnit.test( "ok test", function( assert ) {
// 通る
assert.ok( true, "true succeeds" );
assert.ok( "non-empty", "non-empty string succeeds" );
// 失敗
assert.ok( false, "false fails" );
assert.ok( 0, "0 fails");
assert.ok( NaN, "NaN fails");
assert.ok( "", "empty string fails");
assert.ok( null, "null fails");
assert.ok( undefined, "undefined fails");
});
equal( actual, expected [, message ] )
// equal(): trueかどうかだけど、2つの値を比較してtrueかどうか
QUnit.test( "equal test", function( assert ) {
// 通る
assert.equal( 0, 0, "Zero, Zero; equal succeeds" );
assert.equal( "", 0, "Empty, Zero; equal succeeds" );
assert.equal( "", "", "Empty, Empty; equal succeeds" );
assert.equal( 0, false, "Zero, false; equal succeeds" );
// 失敗
assert.equal( "three", 3, "Three, 3; equal fails" );
assert.equal( null, false, "null, false; equal fails" );
});
// strictEqual() [===] もある
deepEqual( actual, expected [, message ] )
// deepEqual(): objectのkey,valueや配列や関数の比較
QUnit.test( "deepEqual test", function( assert ) {
var obj = { foo: "bar" };
var f = hoge;
assert.deepEqual( obj, { foo: "bar" }, "Two objects can be the same in value");
assert.deepEqual( f, test, "Two objects can be the same in value");
});
function hoge() { }
これらを覚えておけばとりあえずOK。
アサーションの数を指定
アサーションの数を指定して、指定された数と実行される数が違う場合、テストに失敗します。
expect( amount )
// expect(): 同期処理 アサーションの数を指定する,数が違えば失敗
QUnit.test( "a test" , function( assert ) {
assert.expect( 2 );
function calc( x, operation ) {
return operation( x );
}
var result = calc( 2, function( x ) {
assert.ok( true, "calc() calls operation function" );
return x * x;
});
assert.equal( result, 4, "2 square equals 4" );
});
QUnit.test( "a test", function( assert ) {
assert.expect( 1 );
var $body = $( "body" );
$body.on( "click", function() {
assert.ok( true, "body was clicked!" );
});
$body.trigger( "click" );
});
グルーピング
テストのグループ化もできます。
module( name, hooks )
moduleメソッドが呼ばれた地点からグルーピングされ、
出力結果でグループごとにまとめて表示してくれます。
また、グループ単位でテスト前・テスト後に実行する処理(hooks)を定義することも出来ます。(beforeEach
, afterEach
)
// module()
QUnit.module( "group a" );
QUnit.test( "a basic test example", function( assert ) {
assert.ok( true, "this test is fine" );
});
QUnit.test( "a basic test example 2", function( assert ) {
assert.ok( true, "this test is fine" );
});
QUnit.module( "group b" );
QUnit.test( "a basic test example 3", function( assert ) {
assert.ok( true, "this test is fine" );
});
QUnit.test( "a basic test example 4", function( assert ) {
assert.ok( true, "this test is fine" );
});
// テスト前・テスト後に実行する処理(hooks)を定義
QUnit.module( "module A", {
beforeEach: function() {
// prepare something for all following tests
},
afterEach: function() {
// clean up after each test
}
});
他にもいろいろ
非同期テスト、ユーザアクションのテスト、カスタムアサーションなど、いろいろできちゃいます。
詳しくは QUnit API documentation、Cookbook を読んでみてください。
実例
前回 の記事を参考にしてみてください。
すでに存在するテストコードから学べるものは多いです。
(過去記事では jQuery UI でのユニットテストについて触れています。)
Next Step
- Plugin を使う (canvasのテストやRailsとの連携が気になっています。出力結果のカラーテーマを変えれるのも嬉しいところ。)
- JSHintと組み合わせる
- Gruntやgulpでの自動化
Plugin や自動化については後日記事を書きたいと思います。