at posts/single.html

Jasmine で tDiary の JavaScript をテストする

tDiary に jQuery がバンドルされるようになって、 JavaScript を使った tDiary プラグインも増えてきた。 jQuery をバージョンアップすると動かなくなるプラグインが出てきたので、 JavaScrpit 部分のテストをちゃんと書くことにした。

少し調べると、 QUnit と Jasmine が候補で見つかった。

  • QUnit … jQuery が使っているテストフレームワーク
  • Jasmine … RSpec 風の BDD フレームワーク

Jasmine は RubyKaigi2010 で紹介されていた記憶があるな。つーか、自分の日記にもしっかり書いてあった

午後は前述の @sarahmei さんに jasmine についていろいろと質問。 jasmine は JavaScript 用のテストフレームワーク。 RSpec 風に書けるのが特徴。 esm さんのセッションでも使っていた pivotal tracker の開発元 (Pivotal Labs) がメンテしている。 調べてみたら、 Pivotal Labs は昔 JSUnit のメンテをしていたみたいで、いまは jasmine に移行しているようだった。 午前中に jasmine を使って tDiary の下書きプラグインのテストを書いていたんだけど、そのときに出てきた疑問を質問した。

Q. $(function() { var Draft; }) と、 jQuery の ready 関数内で定義したオブジェクトのテスト方法は? A. グローバルにtDiary.Draftと定義するといいよ。(プロダクト名をグローバルな名前空間として使う)

Q. jQuery のような外部ライブラリを組み込んでテストするには? A. jasmine.ymlの src_files で指定する。ただし http:// 指定は不可で、ローカルに jQuery.js を配置する必要あり。 理由を聞いたら src_files には dist/**/*.js のようにワイルドカード指定を許容しており、これを Dir.glob で読み込んでいるかららしい。

Q. jasmine はモデルのテストを書けるようだけど、ビューのテストは可能? A. jasmine-jquery の fixture を使えば、 HTML の断片を組み込むことができる。

おかげでだいたい、 jasmine の使い方が分かった。 つたなすぎる英語での質問だったけど、根気よくかつ丁寧に答えてくれた @sarahmei さんに感謝。

2年も経つとそんなこともすっかり忘れているもので、僕の MacBook Pro 内の tdiary-core ディレクトリに spec/javascript を見つけて「いつの間に tDiary に JavaScript テストが組み込まれたんだろう?」と不思議に思っていた。犯人は2年前の僕でした。日記は付けておくものだね。

あれから2年経って Jasmine のバージョンも1.0から1.2に変わっている。改めて Jasmine を tDiary に組み込んで、今度はちゃんと pull request を送った。 QUnit ではなく Jasmine にしたのは、「gemで配布されていて ruby と親和性がある」「RSpec風の構文が使える」の2点。

Jasmine のインストール

gem install jasmine でもいいんだけど、ライブラリの依存関係を管理するのが面倒なので Bundler を使う。 Gemfile に gem 'jasmine' と書いて bundle install を実行。インストール後に jasmine init を実行すると、ジェネレータによってテストのひな形が生成される。

$ jasmine init
Jasmine has been installed with example specs.

To run the server:

rake jasmine

To run the automated CI task with Selenium:

rake jasmine:ci

初期のディレクトリ構成はこうなっている。

$ tree
.
├── Rakefile
├── public
│   └── javascripts
│       ├── Player.js
│       └── Song.js
└── spec
    └── javascripts
        ├── PlayerSpec.js
        ├── helpers
        │   └── SpecHelper.js
        └── support
            └── jasmine.yml

6 directories, 6 files

特に重要なのは、以下3つのディレクトリ。

  • public/javascripts … テスト対象となる JavaScript ソース
  • spec/javascripts … テスト (spec)
  • spec/javascripts/helpers … テスト用のヘルパー

これらディレクトリの場所は、 spec/javascripts/support/jasmine.yml で変更可能。

テストの実行

rake コマンドを使ってテスト用のサーバを起動する。

$ bundle exec rake jasmine
your tests are here:
  http://localhost:8888/
[2012-09-02 11:44:02] INFO  WEBrick 1.3.1
[2012-09-02 11:44:02] INFO  ruby 1.9.3 (2012-02-16) [x86_64-darwin11.3.0]
[2012-09-02 11:44:02] INFO  WEBrick::HTTPServer#start: pid=70285 port=8888

Web ブラウザでこのサーバに接続すると、 spec/javascript に配置したテストが実行され結果が表示される。

Jasmine

テスト結果として Player オブジェクトの spec が表示されている。 rspec の -fd オプションに似ている。

この画面の HTML ソースを開くと、以下の順番で処理されていることが分かる。

  • jasmine のフレームワーク (jasmine.js, jasmine.css) を読み込んで初期化
  • public/javascripts/*.js のテスト対象を読み込む (サンプルでは Player.js と Song.js)
  • spec/javascripts/helpers/*.js のヘルパーを読み込む(サンプルでは SpecHelper.js)
  • spec/javascripts/*.js の spec ファイルを読み込む(サンプルでは PlayerSpec.js)
  • onload イベントを契機に jasmineEnv.execute(); を実行

Ruby 部分の役割

だいたい Jasmine の動きが分かってきた。 Jasmine 本体は JavaScript で動いていて、テスト環境を用意するところを Ruby が担っている。 Ruby 部分はこんなところか。

  • jasmine init によるテストひな形の生成
  • rake jasmine によるテストサーバの起動
    • テストサーバの役割は Jasmine 本体とヘルパー、 spec ファイルの読み込み

rake jasmine の代わりに rake jasmine:ci を実行すると、テストサーバだけでなく Web ブラウザも起動し、テスト結果をコンソールに表示してくれる。また、テストサーバの動作(各種ディレクトリの場所)は、spec/javascripts/support/jasmine.yml で変更できる。

Jasmine は(Rubyを使わない)スタンドアロン版も配布されている。スタンドアロン版ではテストサーバ相当の HTML (SpecRunner.html) を自分で書けばよさそう。

tDiary への組み込み

こんな手順で tDiary に Jasmine を組み込んだ。

  1. Gemfile に Jasmine を追加
  2. Rakefile (tdiary/tasks/jasmine.rake) に jasminejasmine:ci タスクを追加
  3. ローカルにテスト用の jquery.js を配置
  4. jasmine.yml を編集

最初に jQuery を読み込むように src_files で明示的に指定している。でないと、jQuery に依存する JavaScript ファイルがエラーとなる。

src_files:
    - js/jquery-1.8.js # load first
    - js/00default.js

テスト内容はこんな感じ。 RSpec 風なのでそれほど悩まない。

describe("$tDiary", function() {
  beforeEach(function() {
    // 毎回実行する処理はここに書く
  });

  it("should exist a global $tDiary object", function() {
    expect($tDiary).toEqual(jasmine.any(Object));
    expect($tDiary.plugin).toEqual(jasmine.any(Object));
  });
});

describe("$", function() {
  describe("#makePluginTag", function() {
    describe("when Wiki style", function() {
      beforeEach(function() {
        $tDiary.style = 'wiki';
      });

      it('should create wiki style plugin tag', function() {
        var tag = $.makePluginTag("plugin_name", ["param1", "param2"]);
        expect(tag).toEqual('{{plugin_name "param1", "param2"}}');
      });
    });
  });
});

ここまでは DOM に依存しないテストだった。 DOM に依存する時は spec にて createElement で書く必要がある。 でもそれは面倒なので、次は jasmine-jquery を使って外部の HTML ファイルを読み込めるようにするか。

参考: jasmine-jqueryというかloadFixturesが便利 ::ハブろぐ

関連する日記