この投稿は RSpec Advent Calendar 2016 (幻の)7日目の記事です。
自分用メモとして投稿しようと思ったのですが、ちょうど今日のAdvent Calendarが空いていたので飛び入り参加させていただきました。(そういう意味で 幻の7日目
です )
RSpecを書いていてよく「〇〇したいんだけど、どうやるんだっけ?」とググることが多かったのでまとめてみました。
RSpecの基本文法を理解している中級者以上向けの内容と思います。
RSpecは3.4を使っています。(ちょっと古くてすみません)
前提として以下のクラスがあるとします。
サンプルコードに出てくる foo
という変数はこのFooクラスのインスタンスです。
class Foo
def bar
1
end
def baz(arg)
2
end
end
複数インスタンスのメソッドが呼ばれる度に異なる値を返す
クラスFoo
の複数インスタンスでメソッドbar
が呼ばれる度に異なる値を返したい場合のやり方です。
まず、単一インスタンスであれば
allow(foo).to receive(:bar).and_return(:a, :b, :c)
でOKですが、これを単純に
allow_any_instance_of(Foo).to receive(:bar).and_return(:a, :b, :c)
expect(Foo.new.bar).to eq :a
# -> OK
expect(Foo.new.bar).to eq :b
# -> Failure
としても期待通りには動きません。
そこで以下のようにします。
values = [:a, :b, :c]
allow_any_instance_of(Foo).to receive(:bar) { values.shift }
expect(Foo.new.bar).to eq :a
# -> OK
expect(Foo.new.bar).to eq :b
# -> OK
ブロック/Procで渡せばbarが呼ばれる都度評価されるので、配列の先頭から取り出されていきます。
メソッドの引数に応じてスタブ値を変える
(上記の「複数インスタンスのメソッドが呼ばれる度に異なる値を返す」とあまり変わりませんが。。。)
元のメソッド引数もそのまま渡されるので、それを使用します。
values = { a: 10, b: 20 }
allow(foo).to receive(:baz) { |arg| values[arg] }
expect(foo.baz(:a)).to eq 10
expect(foo.baz(:b)).to eq 20
複数メソッドをまとめてスタブする
allow(foo).to receive_messages(hoge: 1, fuga: 2)
expect(foo.hoge).to eq 1
expect(foo.fuga).to eq 2
メソッドチェインをまとめてスタブする
allow(foo).to receive_message_chain(:bar, :to_s).and_return(10)
expect(foo.bar.to_s).to eq 10
元のメソッドを呼ぶ(スタブしない)
任意のメソッドが呼ばれたかのチェックはされますが、スタブされずそのまま元のメソッドが呼ばれます。(サンプルコードは簡略化しすぎて自明になってますが・・・)
allow(foo).to receive(:bar).and_call_original
expect(foo.bar).to eq 1
複数のmatcherをand/orでつなぐ
mathcherはand/orでつなぐことができます。ただし not_to
(to_not
)の場合は繋げられません。
失敗時には対応したmatcherのメッセージがちゃんと表示されます。
expect(1).to be_present.and be_integer.and be_odd
expect(0).to be_blank.or be_zero
# 以下のように書くのと同じです。
expect(1).to be_present
expect(1).to be_integer
expect(1).to be_odd
matcherをネストさせる
ちなみに receive().with()
で使用できるArgument Matcherも使用できます。
val = [
{ foo: "foofoo", bar: { hoge: 10, fuga: 20 }, baz: 0 },
{ foo: "beebee", bar: { hoge: 30, fuga: 40 }, baz: [5, 6, 7] }
]
expect(val).to match_array([
include(foo: be_include("oo"), bar: kind_of(Hash)),
include(foo: be_include("ee"), baz: array_including(5))
])
こういうこともできるんだよ、という意図で本記事を書きましたが、RSpecで凝ったことをやろうとすることは対象コードのテスタビリティが低いということかもしれず、設計を見直した方がいいかもしれません。