17 Jun 2012

RSpecのequal, eql, eq, be の違い

RSeqでshould equalと書くべきかeqlと書くべきか、それともbeと書くべきか時々混乱するのでこの際覚えてしまおうと意味で何がちがうんだろうと見てみた。それによるとごく簡単にまとめると以下のような結果だった。

二つの変数を比較するとき;


  • 変数の値だけを比べる
  • eql eq
  • 変数の値だけでなく、インスタンスのobject_idまで比べる
  • equal be
と、ここまで覚えておけばSpec書くのに支障はない。


もう少し掘り下げると、それぞれ以下のような仕組みになっている。

eql

Matcherの場所はここ。実際にはObject#eql?を呼んでいる。

> arr.eql? :a => 1, :b => 2
=> true


equal

Matcherの場所はここ。実際にはObject#equal?を呼んでいる

> arr.equal? :a => 1, :b => 2
=> false


eq

eqの定義はDSLの方にあるらしい。


be

Matcherの定義はここ。beに引数があればBeSameAs#matches?がshouldによって呼ばれる仕組みになっている。




整理して実際に試してみよう。以下のようにString, Array, Fixnumの型のインスタンスについてそれぞれeq, eql, equal, beのマッチャでSpecを実施してみた。

# ./tmp_spec.rb
require 'rspec'

describe 'eq' do
  it('should find two strings are equal') { "".should eq ""}
  it('should find two Arrays are equal') { {:a => 1}.should eq :a => 1 }
  it('should find two Fixnums are equal') { 1.should eq 1 }
end

describe 'eql' do
  it('should find two strings are equal') { "".should eql ""}
  it('should find two Arrays are equal') { {:a => 1}.should eql :a => 1 }
  it('should find two Fixnums are equal') { 1.should eql 1 }
end

describe 'equal' do
  it('should find two Strings are equal') { "".should equal ""}
  it('should find two Arrays are equal') { {:a => 1}.should equal :a => 1 }
  it('should find two Fixnums are equal') { 1.should equal 1 }
end

describe 'be' do
  it('should find two strings are equal') { "".should be ""}
  it('hould find two Arrays are equal') { {:a => 1}.should be :a => 1 }
  it('should find two Fixnums are equal') { 1.should be 1 }
end


結果は以下のとおり。equalとbeでは String, Arrayではobject_idまで同じかチェックしている。
$ rspec tmp_spec.rb -f doc

eq
  should find two strings are equal
  should find two Arrays are equal
  should find two Fixnums are equal

eql
  should find two strings are equal
  should find two Arrays are equal
  should find two Fixnums are equal

equal
  should find two Strings are equal (FAILED - 1)
  should find two Arrays are equal (FAILED - 2)
  should find two Fixnums are equal

be
  should find two strings are equal (FAILED - 3)
  hould find two Arrays are equal (FAILED - 4)
  should find two Fixnums are equal

Failures:

  1) equal should find two Strings are equal
     Failure/Error: it('should find two Strings are equal') { "".should equal ""}
       
       expected # => ""
            got # => ""
       
       Compared using equal?, which compares object identity,
       but expected and actual are not the same object. Use
       'actual.should == expected' if you don't care about
       object identity in this example.
     # ./tmp_spec.rb:16:in `block (2 levels) in '

  2) equal should find two Arrays are equal
     Failure/Error: it('should find two Arrays are equal') { {:a => 1}.should equal :a => 1 }
       
       expected # => {:a=>1}
            got # => {:a=>1}
       
       Compared using equal?, which compares object identity,
       but expected and actual are not the same object. Use
       'actual.should == expected' if you don't care about
       object identity in this example.
       
       
       Diff:{:a=>1}.==({:a=>1}) returned false even though the diff between {:a=>1} and {:a=>1} is empty. Check the implementation of {:a=>1}.==.
     # ./tmp_spec.rb:17:in `block (2 levels) in '

  3) be should find two strings are equal
     Failure/Error: it('should find two strings are equal') { "".should be ""}
       
       expected # => ""
            got # => ""
       
       Compared using equal?, which compares object identity,
       but expected and actual are not the same object. Use
       'actual.should == expected' if you don't care about
       object identity in this example.
     # ./tmp_spec.rb:22:in `block (2 levels) in '

  4) be hould find two Arrays are equal
     Failure/Error: it('hould find two Arrays are equal') { {:a => 1}.should be :a => 1 }
       
       expected # => {:a=>1}
            got # => {:a=>1}
       
       Compared using equal?, which compares object identity,
       but expected and actual are not the same object. Use
       'actual.should == expected' if you don't care about
       object identity in this example.
       
       
       Diff:{:a=>1}.==({:a=>1}) returned false even though the diff between {:a=>1} and {:a=>1} is empty. Check the implementation of {:a=>1}.==.
     # ./tmp_spec.rb:23:in `block (2 levels) in '

Finished in 0.01339 seconds
12 examples, 4 failures

Failed examples:

rspec ./tmp_spec.rb:16 # equal should find two Strings are equal
rspec ./tmp_spec.rb:17 # equal should find two Arrays are equal
rspec ./tmp_spec.rb:22 # be should find two strings are equal
rspec ./tmp_spec.rb:23 # be hould find two Arrays are equal


でもこのFixnumの比較に関しては Specが成功してしまう。
describe 'be' do
  it('should find two Fixnums are equal') { 1.should be 1 }
end

これはFixnumのidがrubyを起動時に実行されて既に予約済みだからのようだ

irb/ pry
[28] pry(main)> 1.object_id
=> 3
[29] pry(main)> 2.object_id
=> 5
[30] pry(main)> 3.object_id
=> 7
[35] pry(main)> 1.object_id
=> 3


[32] pry(main)> 1.0.object_id
=> 105431370

このようにFixnumのインスタンスはシングルトンで定義されているようだ。

15 Jun 2012

ひとりごと on RSpec

ああ かれこれ1時間くらい CapybaraのRSpec上でActioveRecord::Baseなクラスの値がchangeしないパターンにはまってた。

こんな場合;
ユーザのパスワードリセッタを実装するときにone_time_tokenを発行してメールを送信する。メール中のtokenとデータベイスに保存されたtokenが一緒かチェック。同じだったら、パスワードを再設定する画面に進む。他にもセキュリティを守るための処理はあるけど基本的な処理はそんなかんじ。

Specはこんな感じ
let(:user) { User.make } # => machinist/acive_record

 it "user's token should be updated" do
  expect{ click_button 'メール送信' }.to change{ user.one_time_token }
end

expect{ click_button 'メール送信'; user.reload }.to change{ user.one_time_token }
# => Got: one_time_token is still ""


正しくはuser.reloadを追加しないといけない。
it "user's token should be updated" do
  expect{ click_button 'メール送信'; user.reload }.to change{ user.one_time_token }
end

changeの中は遅延評価されたuser を参照しているからRSpecの空間で故意にreloadしないと one_time_tokenがActiveRecord上では変わらない。普通の人はすぐ気づくのかなあ?

10 Jun 2012

このRSpecのスライドがすばらしい

Kerry BuckleyさんのRSpecのスライドが素晴らしい。さらっと読み通しておもしろいなと思ったところをノートしてみました。

なお、console でRSpecを使いたいときはSpec::Matchersをincludeすればよいです。
require 'rspec'
include Spec::Matchers

- 表紙
このスライドはSplippyというやつでできている。Escを押すとページのリストが出てかっこいい。

- Context
contextはdescribeのエイリアス

- Formatters
フォーマッタ: docは試験内容がdocument化されて読みやすい。

- Formatters
フォーマッタ: fuubarでプログレスバーをパーセンテイジ表示できる

- Formatters
フォーマッタは複数指定できる

- Profiling
プロファイルで時間のかかってる試験を発見

- Pending specs
itを実装しないとペンディングになる

- Running one spec
行を指定して一つのスペックを実施

- Built-in matchers
equalのmatcherはいろいろある。けど区別できるようにしとく必要ありそう

- Another digression
カスタムメッセージを指定してして後日スペックが通らなかったときに何がダメかわかりやすいメッセージを指定できる

- Back to built-in matchers
Matcherは基本的に既に用意されているbuilt-in matchersを使う。

- Custom matchers
 Custom Matcherはこんな感じで簡単に書けるようになってる

- Implicit subject
当然だけどsubject {} だけじゃなくdescribe :subject {}もそのscopeでsubject(主語)になる

だから以下の二つは等価
describe Array do
  it 'should respond to :each_with_index' do
    should respond_to :each_with_index
  end
end

describe 'An array' do
  subject { Array }
  it 'should respond to :each_with_index' do
    should respond_to :each_with_index
  end
end

- Implicit subject
its "uniq.size"と書くとevalしてくれる

- Explicit subject
subject {} で定義した変数へはsubjectでアクセスできる。

itsが廃止予定なので便利かも its "uniq.size" => subject.uniq.size

- Shoulda matchers
Shoulda MatchersがほとんどのRails Matchersを提供してくれてる

- Rails controllers
Anonymous controllerが使える。例えばあるアクションで異常系の処理のSpecを書きたいときなどに便利


- Learning more
残りは本家で
RSpec
RSpec Rails


.

Rubyに優しいキーバインディングのセッティング

Linuxで '-'と'_'、':'と';'をスワップしておくとコード書くとき便利です。

# ~/.bashrc

# Key bindings
xmodmap -e 'keycode 47 = colon semicolon'
xmodmap -e 'keycode 20 = underscore minus underscore minus backslash questiondown'

4 Jun 2012

RSpecのshouldは何をしてるの? RSpecの仕組み

RSpecのshouldはどうやって動いているのか?..という仕組みについてpaperboy&co.の方が既にcodeを読んで解説されているスライドを見つけました。

まずshouldが全てのinstaceで実効可能なのはKernel classに対して定義されているからです。

shouldの居場所
# spec/expectations/extensions/kernel.rb
def should(matcher=nil, message=nil, &block)
  Spec::Expectations::PositiveExpectationHandler.handle_matcher(self, matcher, message, &block)
end

PositiveExpectationHandler#handle_matcherの居場所
# spec/expectations/handler.rb
match = matcher.matches?(actual, &block)



RSpecの構造

/lib
└── spec
    ├── expectations
    │   └── extensions
    └── matchers
        └── extensions

expectations/extensionsはその名のとおり拡張を行う。spec/expectations/extensions/にはkernel.rbのみがある

RSpecの構造
spec/matchersには見慣れた名前が。。

ずっとshouldはきっと variable.be_a Stringてかくと variable.is_a? Stringって変えてくれると想像して信じていたけど、実際の条件は逐一Matcherに書いてあるらしい。例えばbe_kind_of matcher
def be_a_kind_of(expected)
  Matcher.new :be_a_kind_of, expected do |_expected_|
    match do |actual|
      actual.kind_of?(_expected_)
    end
  end
end

be Matcherには僕が想像していたような機能がある

§

Matcherを自作したい時は Object.should be_fine => Object.fine?の法則に当てはまるfine? methodを持つObject定義してあげるか、matches?にrespondするMatcherを書いてmatcher.rbみたいに requireしてあげればいいはず。


§


蛇足だけど

上記 kernel.rbのとおり引数にmessageを渡してfailure時に何が失敗したのかわかりやすくすることもできる

別のshould、Subject#shouldの居場所
これは以下の用に書いたときのshouldにちがいない(と信じてる)
let(:int) { 16 }
subject { int }

it "should be even" do
  should be_even
end


Change

例えばこんな感じに書いたときは
expect{ array << 42 }.to change{ array.size }.from(0).to(1)
initializeで @value_proc に{ array << 42 }が代入されてfrom methodで@fromに0が、to methodで@to に1が代入される。比較は他のmatchers同様matches?で @beforeと@afterを比較して行われる。event_procには array.sizeが代入される。
@before = evaluate_value_proc
event_proc.call
@after = evaluate_value_proc
スライドすばらしいので全部読むべし