Ruby 以外のプログラムを書いていると, 「RSpec で spec が書けない」というだけの理由で, 億劫になってしまうことがあります.
RSpec で JavaScript の spec が書ければ… そう思って, やってみました.

まずは実際のコードを説明なしに紹介し, 環境構築手順や, 解説については後述します.

テスト対象 (System Under Test)

今回はこの JavaScript のコードを対象に, spec を書いてみます.

とりあえずは「RSpec で JavaScript の spec が書けるのか」という検証が目的なので, シンプルなもので十分でしょう.
t-wada さんのRSpec の入門とその一歩先へをそのまま JavaScript にしてみたようなものです.

detect メソッドに渡す文字列の中に, コンストラクタに渡された単語が含まれるか, を検証するだけの簡単なオブジェクトです.

$ js
Rhino 1.7 release 2 2010 09 15
js> load("message_filter.js")
js> var filter = new MessageFilter("foo");
js> filter.detect("foo bar baz");
true
js> filter.detect("Hello, World!");
false

spec

上記のコードをテストするための spec です.

MessageFilter というクラスは Ruby 上で定義されているわけではないので, ここでは Symbol 型を利用しています.

これを実行すると, 以下のようになります.

$ rspec -fs message_filter_spec.rb 

MessageFilter with argument "foo"
  should detect from "foo bar baz"
  should detect from "Hello World!"

Finished in 0.40217 seconds
2 examples, 0 failures

一応の動作は確認できましたね.

環境の構築

今回は以下のような環境を使用しています.
OS は Ubuntu 10.10 です.

$ ruby -v
ruby 1.8.7 (2011-02-18 patchlevel 334) [i686-linux]
$ rspec -v
2.5.1
$ gem list # 一部省略

*** LOCAL GEMS ***

harmony (0.5.6)
johnson (2.0.0.pre3)
rspec (2.5.0)
rspec-core (2.5.1)
stackdeck (0.2.0)

普段は Ruby 1.9.2 を使用しているのですが, 今回使用する harmony という gem の依存ライブラリである johnson が, 1.9.x ではインストールできないようなので, 1.8.7 を使用しています.
RVM を利用して複数の Ruby 環境を切り替えられる環境であれば, さほど問題は無いでしょう.

以下のようにインストールします. (Ruby, RVM のインストールは省略)

$ gem install stackdeck
$ gem install johnson -v "2.0.0.pre3"
$ gem install harmony
$ gem install rspec

harmony のインストール時には, 依存ライブラリである stackdeck と johnson も一緒にインストールされるべきですが, 現在は問題があってうまくできないようです.
とりあえず手動でインストールしておけば問題ありません.

どういう仕組みで動いているのか

Johnson が Ruby 上での JavaScript (SpiderMonkey) の実行環境を提供し, Harmony はそれをラップした DSL, とのことです.

Harmony::Page オブジェクトが .js ファイルを読み込んだコンテキストを保持し. x メソッド (execute_js メソッドのエイリアス) でプログラムを実行することができます.
x メソッドで実行した JavaScript プログラム中, 最後に評価された値が返り値として Ruby に渡されます.
(ただし, オブジェクトは返らないらしい)

問題点

  • 可読性が低い
    JavaScript のコードを文字列リテラルで書かざるを得ないこともあり, とても読みやすいとは言えないでしょう.
    RSpec はメタプログラミングにより “Test as Documentation” (ドキュメントとしてのテスト) を簡単に書かせてくれるはずですが. ここではその力を発揮できていません.
  • Object の検証ができない
    前述のとおり, Harmony は JavaScript の Object 型の値を Ruby に渡してくれません.
    JavaScript における Object は, ハッシュ変数として使われることもあり, これを検証できないのは致命的でしょう.
    とはいえ, オブジェクトがメソッドを持っていた場合, それを Ruby でどう表現するか, という問題もあり, 解決は難しいでしょう.

上記の理由から, 実際に運用・保守していくのは難しいでしょう.

最近は HTML5 によるスマートフォンアプリ作りに挑戦しようとしているのですが, TDD/BDD のフレームワークに何を使うべきか, で迷っています.
今のところは次作の簡単なフレームワークで検証していますが, さすがにずっと運用するわけにはいかないので, デファクトスタンダードなものが知りたいところです.
いいのを知っている方は是非 @yuya_takeyama まで教えてください.

JavaScript におけるテスト技法については, 2011/03/08 (Tue) に Test.js というイベントでおもしろい話が聞けそうです.
是非参加したいところですが, ATND への参加登録が遅くなったので絶望的な状態です…
この記事を LT する発表者としての参加, なんてのはダメですよねぇ…
Ust で我慢します.

この記事は個人的なメモです.

[エラー: isbn:4320026926:n というアイテムは見つかりませんでした]を読んでいて, 構造体の章に出てきた BinaryTree がよくわからないので, とりあえず Ruby で実装してみた.
単語をキーに, その回数を数えるだけの単純なプログラム.

BinaryTree::Node クラス

ツリー構造のノードを表すクラス.
単語, 回数, そして左/右のノードをプロパティとして持ちます.

RSpec

せっかくなので BDD (振る舞い駆動開発) の練習.

実行するとこのようになります.

$ rspec binarytree_spec.rb --format doc
BinaryTree::Node
  with word "foo" and no children
    it should behave like Node with word "foo"
      word
        should == "foo"
    it should behave like Node with no children
      left
        should be nil
      right
        should be nil
    size
      should == 1
    count
      should == 1
    words
      should == ["foo"]
    count_all
      should == 1
  with word "foo" and inserted a node with word "bar"
    it should behave like Node with word "foo"
      word
        should == "foo"
    it should behave like Node with one child with no children
      size
        should == 2
    right
      should be nil
    words
      should == ["bar", "foo"]
    count_all
      should == 2
    its child on the left
      it should behave like Node with no children
        left
          should be nil
        right
          should be nil
      word
        should == "bar"
      size
        should == 1
      count
        should == 1
      count_all
        should == 1
  with word "foo" and inserted a node with word "hoge"
    it should behave like Node with word "foo"
      word
        should == "foo"
    it should behave like Node with one child with no children
      size
        should == 2
    left
      should be nil
    words
      should == ["foo", "hoge"]
    count_all
      should == 2
    its child on the right
      it should behave like Node with no children
        left
          should be nil
        right
          should be nil
      word
        should == "hoge"
      size
        should == 1
      count
        should == 1
      count_all
        should == 1
  with word "foo" and inserted a node with word "foo"
    it should behave like Node with word "foo"
      word
        should == "foo"
    it should behave like Node with no children
      left
        should be nil
      right
        should be nil
    count
      should == 2
    words
      should == ["foo"]
    count_all
      should == 2
  with word "foo" and inserted a node with word "bar" 3 times
    it should behave like Node with word "foo"
      word
        should == "foo"
    right
      should be nil
    count
      should == 1
    size
      should == 2
    words
      should == ["bar", "foo"]
    count_all
      should == 4
    its child on the left
      word
        should == "bar"
      size
        should == 1
      count
        should == 3
      count_all
        should == 3
  with word "foo" and inserted a node with word "hoge" 3 times
    it should behave like Node with word "foo"
      word
        should == "foo"
    left
      should be nil
    count
      should == 1
    size
      should == 2
    words
      should == ["foo", "hoge"]
    count_all
      should == 4
    its child on the right
      word
        should == "hoge"
      size
        should == 1
      count
        should == 3
      count_all
        should == 3
  with word "a" and inserted nodes with word "b" ~ "j"
    size
      should == 10
    words
      should == ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]

Finished in 0.03146 seconds
57 examples, 0 failures

いやー, RSpec って楽しいですね.
PHP にもこういうの欲しい.

実行してみる

“to be or not to be” というフレーズ中に含まれる単語と, その回数を計算します.
結果は以下のとおり.

$ ruby example.rb
be (2)
not (1)
or (1)
to (2)

4 words.
6 nodes.

今度は C/C++ でも実装してみよう.
そして BTree や B+Tree についても勉強してみよう.

というわけで今日はここまで.

, ,