JanGaJan.com

Is fun? JOY!

Rspec3のexpectとallowの違い

よちよち.rb Advent Calendar 2014 8日目の記事です。
昨日は、bonbon0605さんの2014年に読んだり積んだりした本とその思い出を振り返ります、でした。
Ruby初学者がワンステップレベルアップする時に参考となる良書を紹介していただきました!

さて、今日はRspecのとあるメソッドから学んだ、mockとstubの違いについて書きます。 mockとstubの違いは、少し前に参加したよちよち.rbで疑問のまま終わってしまっていました。

最近になってRubyとRailsの仕事に関わっているのですが、Rspecに苦戦しています。
Rspecを使っている時に、次の2つのメソッドに出会いました。

  • allow(object).to receive(:hoge).and_return(‘fuga’) 参考
  • expect(object).to receive(:hoge).and_return(‘fuga’) 参考

この使い分けを知る過程でstubとmockの違いが自分の中でイメージできてきました。

まずは、上記2つのメソッドの使い方を非常に単純な例に落とし込んでみます。

上記はメソッドはどちらも、object.hogeってメソッドを呼び出すと、戻り値がfugaとなります。
さて、どう違うのか、試してみます。

事前準備

問題を単純化します。(逆にわかりづらいかもしれないし、ソースコード減らしたかったのであまりいい書き方ではありません)
truefalseを返すだけのprivateメソッドとそのラッパーメソッドを用意します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MockAndStub
  def always_true
    return truthy
  end

  def always_false
    return falsey
  end

  private
  def truthy
    true
  end

  def falsey
    false
  end
end

それと、テストクラスです。

1
2
3
4
5
6
7
8
9
describe MockAndStub do
  let(:obj) { MockAndStub.new }

  # publicメソッドの戻り値が正しいことを確認
  context 'do not use stub and mock' do
    it { expect(obj.always_true).to be_truthy }
    it { expect(obj.always_false).to be_falsey }
  end
end

準備ができました。まずはallowから見ていきましょう。

allow(object).to receive(:hoge).and_return(‘fuga’)

allowはstubとして働きます。
allowを使うと、上述の説明どおり、object.hogeというメソッドを呼び出すと、fugaが返ってきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  ...
  context 'use stub' do
    # 1. fail
    before(:each) { allow(obj).to receive(:truthy).and_return(false) }
    it { expect(obj.always_true).to be_truthy }

    # 2. success
    before(:each) { allow(obj).to receive(:falsey).and_return(true) }
    it { expect(obj.always_false).to be_truthy }
  end

  context 'use stub, but not use' do
    # 3. success
    before(:each) { allow(obj).to receive(:falsey).and_return(true) }
    it { expect(obj.always_true).to be_truthy }
  end
  ...

1. fail

1のパターンのテストはfailします。
allow(obj).to receive(:truthy).and_return(false)とすると、obj:truthyというメソッドコールを受け取ったら、必ずfalseが返ってきます。
always_trueは内部で:truthyを呼び出しているので、戻り値はfalseとなります。
よって、obj.always_truefalseなので、failします。

2. success

今度はsuccessです。:falseytrueを返すと設定しているので、obj.always_falsetrueとなります。

3. success

次はテストに関係無いメソッドをstub化した場合です。これも、関係ないのでsuccessになります。

上記のように、A: allowはメソッドコールがあった時に指定した値を利用してテストすることができます。

expect(object).to receive(:hoge).and_return(‘fuga’)

続いてexpectで、こちらはmockです。
allowexpectの違いは、メソッドコールの検証の有無です。expectではreceiveで指定したメソッドが呼び出されていない場合、failします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  ...
  context 'use mock' do
    context 'use mock, and not use' do
      # 4. fail
      before(:each) { expect(obj).to receive(:falsey).and_return(true) }
      it { expect(obj.always_true).to be_truthy }
    end

    context 'use mock, and use' do
      # 5. success
      before(:each) { expect(obj).to receive(:falsey).and_return(true) }
      it { expect(obj.always_false).to be_truthy }
    end
  end
  ...

4. fail

receiveで指定したメソッドを呼び出さない場合です。
itのブロック自体は正しいため、beforeのexpectallowに変えるか、before自体を消すとsuccessになります。

5. success

receiveで指定したメソッドを呼び出しています。
値をtrueに変更した上で、trueになっていることを確認しているので、successとなります。

まとめ

allowのexpectの違いはメソッドコールの検査になります。これを踏まえた上で、下の記事を読むとなるほど!と思うことができました。

つまり、stubとmockの違いは、インターフェースの検査(メソッドコールのチェック)をするかしないか、ということですね。

ところで、2つの違いはわかったけれど、「どう使い分ければいいのか」が実はもっと大事です。
ただ、これはテストに依存するため、確認すべきことは何か、を意識しながら切り分ける必要があります。
Martin Fowlerの"Mocks Aren’t Stubs"の翻訳記事があって、非常に長い上に全容を理解しきれていないのですが、

振る舞いの結びつきを意識しなければいけないか

という点が判断材料になるかと思います。
あと、

モックオブジェクトはXPコミュニティから出てきたものだ。また、XPの主な特徴の一つは、テストドリブン開発を重要視していることだ。テストドリブン開発では、テストを書くことで推進される反復を通じてシステム設計も進んでいく。 そういったわけで、モックオブジェクトの支持者達が相互作用テストの設計における効果についてことさら語るのも驚くことではない。このスタイルでは、主要なオブジェクトに対する最初のテストを書くから [それらのオブジェクトの] 振る舞いの開発を始めることになる。

とあるので、TDDするならmock使うでいいと思います。

ひとまず、色々試してみて、気になることがあれば、また何か書くかもしれません。

最後に

そして、今日はよちよち.rb 第48回 “よちよちもくもく会”です(予定があって行けなくて残念)

プログラミング未経験でも参加できる一番初心者に優しいRubyの勉強会、もといコミュニティです。
後一枠余っているので、試しに参加して楽しんでみてください。
当日キャンセルがちょこっと出たりすることが多いので、満席でもキャンセル待ちにしておくと、いいことがあるかもしれません。

明日はまだ決まっていません…(紹介したかった…) # TODO:誰か現れたら更新するかも
きっと今日参加する誰かが書いてくれるはずです。
楽しみにお待ち下さい。

Comments