【JavaScript】Jasmine + JSCoverを使ったテスト駆動開発

こんにちわ。竪月です。

ちょっと前に業務でJSのWebクライアントアプリ開発をやっていたとき、テストの工程(UT)はカバレッジ100%を求められました。
そこで、JUnit+Eclipseみたいな、テストコードを書いてカバレッジもさくっと確認できる良い感じのテストフレームワークないかなと探したところ、Jasmine+JSCoverの組み合わせがよかったので、今回は「テスト駆動で書くJavaScript」をテーマに、準備、テストコーディング、カバレッジ確認までの手順を紹介したいと思います。

ちなみに「テスト駆動開発」(Test Driven Development : TDD)とは、アジャイル開発手法のひとつで、最初にテストを書き(レッド:NGの状態)、そのテストの対象となるプログラムを必要最低限書き(グリーン:OKの状態)、洗練させる(リファクタリング)、という短い工程を繰り返す開発スタイルのことです。

TDDが発展した「ビヘイビア駆動開発」(Behavior Driven Development : BDD)も注目されていて、Jasmineの本家サイトには、Jasmine自体がBDD用フレームワークとの紹介がありました。
ただ、今回はそこまで正確な定義ではなく、機能(function)単位でテストを書いて実装する、くらいの感覚で進めていきます。

◆各ツールについて

・Jasmine JavaScript用のテスティングフレームワーク。 Jasmine用に拡張されたjQueryのライブラリもあり、合わせて使うとDOM読み込みも簡単。今回も使います。
・JSCover JavaScriptのコードカバレッジを測定し、レポートを出力してくれるツール。

◆準備

ではさっそく環境を準備していきます。

①Jasmine

以下URLから「jasmine-standalone-2.2.0.zip」をダウンロード、解凍する https://github.com/jasmine/jasmine/tree/master/dist ディレクトリ構成は以下のようになっています。

jasmine

|- lib : テストのcore ライブラリ(※他のライブラリもここに格納する)

|- spec : テストコード(サンプル)

|- src : テスト対象のソースコード(サンプル)

|- SpecRunner.html : テストを実行する

 

②JSCover

以下URLから「JSCover-1.0.17.zip」をダウンロード・解凍し、lib/の下に格納 http://sourceforge.net/projects/jscover/files/JSCover-1.0.17.zip/download 

③jQuery

以下URLから「Download the compressed, production jQuery 1.11.2」をダウンロードし、lib/jasmine-jquery.js を lib/jasmine-2.2.0の下に格納 http://jquery.com/download/

④Jasmine jQuery

以下URLから「Download ZIP」をダウンロード・解凍し、lib/jasmine-jquery.js を lib/jasmine-2.2.0の下に格納 https://github.com/velesin/jasmine-jquery

⑤SpecRunner.html の読み込みファイルに、libに追加したjsを追加

 

⑥JSCoverの実行・確認

環境のセットアップが正しくできたか、試しにサンプルテストを実行してみましょう。

(1) jasmineディレクトリに移動し、javaの以下コマンドを実行してJSCoverを走らせます。

$ cd jasmine/
$ java -Dfile.encoding=UTF-8 -jar lib/JSCover-1.0.17/target/dist/JSCover-all.jar -ws --no-instrument=lib --document-root=. --report-dir=report

※javaオプション「-Dfile.encoding」でソースファイルのエンコード種別を指定します。UTF-8で作りましょう。 ※その他オプションの意味についてはそれぞれ以下の通りです。

  • –document-root : ルート。テストコード、テストコードが参照する前ファイルが格納されたディレクトリを指定する。
  • –report-dir:テスト結果(レポート)を格納するディレクトリを指定する。
  • –no-instrument:測定対象外のディレクトリを指定する。
  • –port:ポート。指定しない場合は8080となるため、都合が悪い場合は別のポートを指定する。

(2) 下記のようにエラーが発生しなければOKです。

2015/03/21 19:10:30 jscover.Main runMain
情報: Args: -ws,--no-instrument=lib,--document-root=.,--report-dir=report

(3) ブラウザ(Chrome)から以下URLを開き、以下のように結果が表示されればOKです。 http://localhost:8080/jscoverage.html?SpecRunner.html utage_jasmine_01 ※上記BrowserとSummary,Source,Storeの説明は「◆カバレッジ確認」にて詳細を記載します。

◆テストコーディング

①Jasmineによるテストコードの書き方
テストを書く前に、Jasmineの書き方について、さらっと触れたいと思います。

(1) テストファイル テストコードは、./jasmine/spec/配下にXxSpec.jsというファイル(ファイル名の決まりはないが、サンプルに倣い)を追加しましょう。 追加したSpecファイルをテスト対象に追加する場合は、./SpecRunner.htmlに追記します。

  (2) SuiteとSpec テストコードはSuiteとSpecで構成され、Suiteはひとまとまりの機能、Specは機能仕様に対する確認観点と考えます。 Suiteはdescribe(テストケース単位)、Specはit(期待結果の単位)となり、以下のようにコーディングをします。 utage_jasmine_05 例)テスト対象ソース(Player.js)とテストソース(PlayerSpec.js)

・beforeEach()、afterEach()
beforeEach()とafterEach()は、それぞれセットアップとティアダウンの役目を担います。 同階層の全describe()、it()の前後に実行されます。

・ describe()とit()
describe(Suite)は関連するメソッド(it(Spec))をグループ化します。 例えば上図④はPlayer.pause()という機能をまとめ、⑤⑥でPlayer.pause()機能の実行結果を判定しています。

・expect()
it()にて、対象のfunction()を実行する前後の期待する状態を判定することで、テスト結果を測定します。 expect()の結果が真とならなければ、NGとなります。 expect()は以下のような判定メソッド(マッチャー)と組み合わせて使用します。

  • .toBe :同じオブジェクト(===)か否か
  • .toEqual :同値(==)か否か
  • .toBeDefined :undefinedでない(定義済み)か否か
  • .toBeNull :nullか否か
  • .toBeTruthy、toBeFalsy :true(false)か否か
  • .toMatch :正規表現に一致するか否か
  • .toContain :配列や文字列に含むか否か
  • .toBeLessThan、toBeGreaterThan :対象より小さい(大きい)か否か
  • .toThrow :例外を発生させるか否か
  • .not :expect()との間に書き、条件を否定する

・spyOn()
また、関数コールを監視するスパイ機能と組み合わせるマッチャーもあります。
spyOn(obj, ‘method’)

obj.method (Functionオブジェクト)に成り代わるスパイが生成され、expect(obj.method)に以下判定を組み合わせます。

  • .toHaveBeenCalled() obj.method()が呼ばれたか否か
  • .toHaveBeenCalledWith(args) obj.method()が引数args指定されて呼ばれたか否か。

②テストのコーディング規約を決める
ルールを決めておくと、複数名で開発する際に統一できるのでオススメです。 コーディング規約の例:

  • テストファイル作成単位:モジュール毎に作成
  • テストファイル名:<module名>Spec.js
  • 第一階層のdescribe名:describe(‘<Module名>’, function() {
  • describe名には[大中小分類][数字]、[ケース][数字]、it名には[期待値][数字]、といった接頭辞をつける([数字]は一意のテスト項番)
  • HelperSpec.jsの利用:共通または、複雑なテスト処理はhelperにテストメソッドを追加する
  • DOMの読み込み:”loadFixtures(HTML_URL)”または、”setFixtures(DOM)”を利用して試験用のDOMを読み込む

③テスト対象の機能仕様を決める
今回は、以下のような機能とします。jQueryを使って実装します。

  • ファイル名:Scroller.js(テスト:ScrollerSpec.js)
  • 機能名:画面スクロール機能
  • メソッド一覧:

指定した要素へスクロールする トップへスクロールする スクロール場所を取得する

④テスト対象の機能仕様からテストコードを書く
実装する機能(function)の仕様から、テストコードを書いていきます。

◆/spec/ScrollerSpec.js

describe("クラス[01] スクロール機能(Scroller)", function() {
  var scroller;

    beforeEach(function() {
        scroller = new Scroller();

        //loadFixtures('index.html');
        setFixtures('
' + '
' + '' + '
' + ''); $pageTop = $('#pageTop'); $scrText = $('#scrText'); $scrButton = $('#scrButton'); }); describe("メソッド[01] scroller.scrollTo($node)", function() { describe('ケース[01] [正常系]パラメタ"$node"に要素を指定する', function () { it('期待値[01] 要素の位置にスクロールされること', function (done) { scroller.scrollTo($scrText); setTimeout(function () { expect(scroller.getPosition()).toBe($scrText.offset().top); done(); }, 1000); }); }); // ★① scrollTo()内の判定条件ケースが不足しているため、Branchのカバレッジが100%にならない }); describe("メソッド[02] scroller.scrollTop()", function() { describe('ケース[01] [正常系]実行する(パラメタなし)', function () { it('期待値[01] 画面トップ(0px)にスクロールされること', function (done) { // トップに移動する試験のため、試験前に位置を移動しておく scroller.scrollTo($scrButton); scroller.scrollTop(); setTimeout(function () { // ★② NGとなるケース(画面トップへスクロールのはずが、謝って期待値に画面トップでない要素を指定している) expect(scroller.getPosition()).toBe($scrText.offset().top); done(); }, 1000); }); }); }); describe("メソッド[03] scroller.getPosition()", function() { describe('ケース[01] [正常系]トップへスクロール後、実行する(パラメタなし)', function () { it('期待値[01] 0が返却されること', function (done) { scroller.scrollTop(); setTimeout(function () { expect(scroller.getPosition()).toBe(0); done(); }, 1000); }); }); }); });

◆実装

(1)テスト対象の機能を実装する テストコーディング4の全ケースがOKとなるような機能を実装します。

◆/src/Scroller.js

/** スクロール機能を提供するクラス */
function Scroller() {
}

/**
 * 指定先にスクロールする。
 *
 * @param {jQuery} $node スクロール先要素(jQuery)
 */
Scroller.prototype.scrollTo = function($node) {
    //指定要素へスクロール
    this.postion = 0;
    if ($node !== undefined && 
        $node[0] !== undefined &&
        $node[0].nodeType === 1) {
        this.postion = ($node.offset()).top;
    }
    $('html,body').animate({ scrollTop: this.postion }, 'fast');
};

/**
 * 画面トップへスクロールする。
 */
Scroller.prototype.scrollTop = function() {
    //ページ上部へスクロール
    this.postion = 0;
    $('html,body').animate({ scrollTop: this.postion }, 'fast');
};

/**
 * スクロールした場所を取得する
 * @return {number} 画面トップからのトップからの位置px
 */
Scroller.prototype.getPosition = function() {
    return this.postion;
};

◆テスト結果、カバレッジの確認

(1) ブラウザ(Chrome)から以下URLを開く
http://localhost:8080/jscoverage.html?SpecRunner.html

(2) Browserタブを確認
Browserタブを開いて、テスト結果を確認します。

utage_jasmine_06 上記サンプルソースを実行すると、上記のようにNG1件となります。 メソッド[01]scroller.scrollTop()、ケース[01]の以下判定に謝りがあったため、以下のように修正する必要があります。

expect(scroller.getPosition()).toBe($scrText.offset().top);

expect(scroller.getPosition()).toBe(0);

修正したら、再度ブラウザを読み込みましょう。 utage_jasmine_07 すべてのテストソースがOKとなりました!  

(3) Summaryタブを確認
Summaryタブを開いてカバレッジを確認します。 utage_jasmine_09 上記サンプルソースを実行すると、Branchのカバレッジが50%となり、全分岐の条件判定を実行したことになりません。

(4) Sourceの確認
カバレッジが取れない場所を確認するには、上記左端の「/src/Scroller.js」リンクをクリックして、Sourceタブを開きます。 utage_jasmine_10 上図の「info」と赤くなっている複数の条件文が通っていない箇所です。 「info」をクリックすると、以下のようなアラートが表示されて原因がわかります。 utage_jasmine_12 複数の条件は全てのケース、かつ、条件が偽となるケースも実装しなければ、カバレッジ100%にすることはできません。 ※条件にあうテストケースを追加するだけなので、修正ソースは割愛します。

(5) Storeタブからレポートの出力
Storeタブを開いて、「Store Report」ボタンをクリックすると、結果が「report」ディレクトリに出力されます。 utage_jasmine_13utage_jasmine_14 以下のように、jscoverage.htmlが生成されます。 utage_jasmine_16

◆まとめ

テスト駆動で書くJavaScript、いかがでしたか。

テストを書きながらコーディングするメリットは、テストケースを大量に書くのが億劫になるので、分岐を減らす、複雑な条件は簡潔にする、ネストを深くしない、機能(function)単位を小さく、といったことを意識して書くことができることかなと思います。(本来のTDDでは、リファクタリングの工程でやるのでしょうが、私は実装の段階で意識しています。)

また、JSは明示的な型指定がないので、テストを書きながらパラメタや戻り値の型や値の正当性を意識することで、早い段階でバグの作り込みを回避できるのも良いですね。

あと、1度テストを書いてしまえば、いつでも、どのブラウザでも再テストできるのが、すごく良いですね。打鍵だと人力に頼らなければならないし、ミスもありますし。。

次回も引き続きJavaScriptをテーマに、エラーや潜在的な問題を検出してくれるJSHintの紹介をしたいと思います。

参考サイト:

本家
http://jasmine.github.io/edge/introduction.html

環境セットアップからテストの書き方まで、さくっと試せる感じでした。 http://monmon.hatenablog.com/entry/2013/12/10/080051

テストコードの例がたくさん書かれているので、参考になりました。 http://qiita.com/opengl-8080/items/cf3acafda9756f4b04c9

コメントを残す

メールアドレスが公開されることはありません。