RSpec を使うための4つの語彙

年末年始にスライドを作ろうと思って挫折しました。

RSpec のテストコードは見慣れない "should" なんかが出てきて難しそうに見えますが、実は覚えなきゃいけないのは 4つ程の語彙だけです。*1

  • Object#should_xxx [args]
  • Object#should_have(n).items
  • Object#should_not_xxx
  • Proc#should_raise()

Object#should_xxx [args]

超基本の書き方です。should_ の後に続いたメソッドを呼び出し、その結果が真ならパスします。args で、そのメソッドヘの引数も与えられます。これだけだとイメージし辛いと思いますのでいくつか例を。
"?"は付いても付かなくても OK だったと思います。

1.should_equal 1

Fixnum#equal を呼び出してます。上記の例では下記の判定がなされてるイメージ*2で。

  1.equal?(1) || raise Spec::Expectations::ExpectationNotMetError
"foo".should_eql "foo"

(2007/01/26 eqlと==の話を修正)
オブジェクトの等価性(という言いかたでいいのかな?)の判定、いわゆる == での判定は Object#eql を使います。
Object#eql? での判定は should_eql() を使用します。

 "foo".should_eql? "foo"

また、ちょっと気持ち悪いですが、下記のような書き方もできます。

 "foo".should == "foo"

それぞれ RSpec の内部で Object#eql?() と Object#== を呼び出していますので、二つの挙動に差がある場合(自作クラスをオーバーライドしたときなど)には注意が必要です。
余談ですが、昔の RSpec は should_equal でこの等価性の判定をおこなえました。個人的にはそっちのほうが好きですが、Ruby で用意されてるメソッドに合わせる、という意味では妥当でしょうかね。

"foo"should_respond_to :sub
 "foo".respond_to?(:sub) || raise Spec::Expectations::ExpectationNotMetError
be動詞の扱い

be は付けても付けなくても良い、いわゆるシンタックスシュガーってやつです。呼び出したいメソッドが形容詞っぽい場合は付けとくとキレイです。

 [].should_be_empty # [].should_empty と等価

 # どちらにしろ下記のような呼ばれ方をする
 [].empty? || raise Spec::Expectations::ExpectationNotMetError

例外的に、be だけを使った場合には Object#equal と同様のオブジェクトの同一性判定をおこないます。

 :sym.should_be :sym
 "str".should_be "str" # これは失敗する

Object#should_have(n).items

このへんからいい意味でキモいエリアに突入します。Object#should_have はコレクションを返すメソッドに対して、その個数が正しいことを検証します。

中で呼ばれるのはこんなイメージ

 object.items.size == n

例えば ActiveRecord なモデルで Blog has_many :articles の場合に、下記のように使ったりします。

context "新規のブログに記事をふたつ追加する場合" do
  setup do
    @blog = Blog.new
    @blog.articles = [Article.new(:one), Article.new(:two)]
  end

  specify "2個の記事があること" do
    @blog.should_have(2).articles
  end
end
中の動作を知りたければ
  {:one => 1}.should_have(2).to_a

が通る、ということから類推できるかと。これはひどい

Object#should_not_xxx

not は否定。そのままです。下記が全部とおります。

  Object.new.should_not_be_kind_of String
  "str".should_not_respond_to :push
  [:one].should_not_be_empty
  {:one => 1}.should_not_have(10).to_a

Proc#should_raise()

Proc#should_raise(Exception) では、処理を実行した結果、例外が出ることを想定している場合に使います。例えばこんな感じ。

  lambda{ nil.some_method }.should_raise( NoMethodError )

あるいはこんな感じで。

  h = Hash.new {|hash, key| raise(IndexError, "hash[#{key}] has no value") }
  lambda{ puts h[1] }.should_raise( IndexError )

もちろん not も使えます。

  h = Hash.new {|hash, key| raise(IndexError, "hash[#{key}] has no value") }
  h[1] = :first
  lambda{ puts h[1] }.should_not_raise( IndexError )

ちなみに一切例外が発生しないこと、という場合には Proc#should_not_raise(Exception)で。

まとめ

このへんが使えると普通に Ruby アプリケーションを書く場合や、Rails でのモデルのテストを書きたい場合には十分な語彙になるかと。実際にそれなりに RSpec 使ってますがこのへんの組合せで 8割は済んでますし。

Rails でガシガシやりたいときには mock や RSpec on Rails で追加される語彙(controller.should_render()とか)も必要になりますが、それはおいおいあとで書く、かも。ただそのへんは API が流動的だったりするので、ちょっとどうしようか迷ってます。

ということで、2007 年のテスト書き初めは RSpec でいかがでしょうか。今年もよろしくお願いします。

*1:RSpec on Rails で、コントローラやビューのテストを書きたい場合はもう少し拡張語彙がありますけど

*2:あくまで使用する側からの見えかたです。内部ではもう少しいろいろがんばってたりします。