Jasmine を使って JavaScript コードをテストしよう
Jasmine ってなに?
Jasmine は JavaScript 用のビヘイビア駆動開発テスティングフレームワークです。
よくテスト駆動開発(TDD)とか言いますが、ビヘイビア駆動開発(BDD)は TDD の派生系で、要求仕様に近い形で自然文を併記しながらテストコードを記述できるという特長があります。
※ 詳しくは Wikipedia - ビヘイビア駆動開発 を参照
テスティングフレームワークってどんなもの?
ここまで進めておいて何ですが、人によってはそもそもテストってなに?という疑問も浮かぶと思います。
表面的というか取っ付きやすい答えとしては、書いたコードの妥当性を保証するのがテストと言えると思いますが、本質的な意味合いとしては仕様・設計を実装に落とし込む助けとなるもの、つまりコードの品質を高めるものだと思います。
後者の意味合いはコチラやアチラを読むと納得しやすいと思います。
では本題に戻り、具体的なテストの例を挙げてみます。
テスティングフレームワークを利用しないテストの例
以下の JavaScript の関数は引数に文字列を渡すと「文字列の先頭もしくは末尾の空白を除き、その他の空白を -(ハイフン)に置き換えて呼び出し元に返す」関数です。
function slugify(str) {
    return str.replace(/^\s+|\s+$/g, '')
              .replace(/\s/g, '-');
}
このコードは slugify("hoge fuga") のように実行すると "hoge-fuga" という置き換えられた文字列が返ってきますので、以下の様に記述すれば勿論 true が返ってきます。
slugify("hoge fuga") === "hoge-fuga"
=> true
この形式を利用して要求仕様を満たしているか以下のコードようにチェックしてみましょう。
// 先頭に空白がある文字列を渡すと先頭の空白を除いた文字列が返ってくる
slugify(" hoge") === "hoge"
=> true
// 末尾に空白がある文字列を渡すと末尾の空白を除いた文字列が返ってくる
slugify("fuga ") === "fuga"
=> true
// 先頭末尾に空白がある文字列を渡すと先頭末尾の空白を除いた文字列が返ってくる
slugify(" moga ") === "moga"
=> true
// 先頭末尾以外に空白がある文字列を渡すと空白をハイフンに置き換えた文字列が返ってくる
slugify("hoge fuga") === "hoge-fuga"
=> true
... 他にもパターンはあります
かなり雑な説明ですが、上記のようなコードを書き易く、見易く仕組み化したものがテスティングフレームワークです。
Jasmine でテストを書いてみよう
グダグダ説明するよりも実際にやってみたほうが早いってことで、Jasmine でテストコードを書いてみましょう。
Jasmine でテストコードを書く際は、3 つの関数を覚えるだけで大丈夫です。
その 3 つの関数は describe と it、expect です。
describe と it、expect
describe は仕様のグループを定義し、it は仕様を定義します。
describe('ユーザモデルテスト', function() { // 同種のテストをまとめる
    it('ユーザのタイプは 2 種類ある', function() {
        ~テストコード~
    });
    describe('ユーザのニックネームは', function() { // ネストできる
        it('半角英数字とハイフンのみ使用可能である', function() {
            ~テストコード~
        });
        it('先頭にハイフンは使用できない', function() {
            ~テストコード~
        });
    });
});
そして具体的なテストコードを書く際には it の中で expect を利用します。
expect は、expect の第一引数として渡された値の期待される振る舞いを評価するためのメソッド群を返します。
そのメソッド群は Jasmine.Matchers と呼ばれます。
expect("hoge").toEqual("hoge");
=> true
Jasmine.Matchers にはたくさんの種類が存在します。
Jasmine.Matchers の種類
期待される振る舞いを評価するときは「~と同じ」以外にも「~を含む」や「~と同じではない」など色々な手法を使います。
そこで、Jasmine.Matchers が提供する評価の種類を以下にリストアップします。
| 種類 | 利用例 | 説明 | 
|---|---|---|
| toBe | expect(A).toBe(B) | A が B と同一であることを期待する | 
| toEqual | expect(A).toEqual(B) | A が B と同値であることを期待する | 
| toMatch | expect(A).toMatch(B) | A が B とマッチすることを期待する | 
| toBeDefined | expect(A).toBeDefined() | A が定義済みであることを期待する | 
| toBeUndefined | expect(A).toBeUndefined() | A が未定義であることを期待する | 
| toBeNull | expect(A).toBeNull() | A が null であることを期待する | 
| toBeTruthy | expect(A).toBeTruthy() | A が true であることを期待する | 
| toBeFalsy | expect(A).toBeFalsy() | A が false であることを期待する | 
| toContain | expect(A).toContain(B) | A が B を含んでいることを期待する | 
| toBeLessThan | expect(A).toBeLessThan(B) | A が B より小さいことを期待する | 
| toBeGreaterThan | expect(A).toBeGreaterThan(B) | A が B より大きいことを期待する | 
| toBeCloseTo | expect(A).toBeCloseTo(B) | A が B と与えられた少数桁数まで同値であることを期待する | 
| toThrow | expect(A).toThrow(B) | A が例外を発生させることを期待する(B が渡されている場合はメッセージが一致するかまで判定する) | 
これらのメソッドを利用できます。
しかし、上の表は全て肯定形のテストです。
否定形のテストには not を利用します。
expect("hoge").not.toEqual("fuga")
=> true
否定用に新しくメソッド名を覚える必要がなくて楽ですね。
ちなみに Jasmine でテストコードを書いてブラウザで実行するとこうなります。
このようにテスティングフレームワークを利用すると、レポーティング機能までついてきて分かり易いです。
ちょっと飽きたのでここまでて。
以上。