rspecでモックを使うときに気をつけていること
スタブとは
テストコード内で利用されるもの。
テスト対象のメソッドや処理から、別のメソッドを介してデータを取得したり加工したりする場合に、決まりきった値を返すように設定したものを指す。
class A
def initialize
@b = B.new
end
def hoge
...
@b.fuga
...
end
end
A#hoge
のユニットテストを書く場合に、B#fuga
の細かい挙動については知る必要がなく、A#hoge
のテストをするに当たって都合のいい値さえ返してくれればいい。(もちろん B#fuga
別途ユニットテストを書く必要がある。)
テストを記述する際に、そのテストで確認したい内容のために何か都合のいい値を返すように設定したものがスタブである。
モックとは
モックもテストコード内で利用されるもの。
あるメソッドを呼び出す際に、その引数や呼びされる回数を検証するために設定したものがモックである。
上記の例だと、B#fuga
は A#hoge
の中で、引数なしで1回だけ呼び出されている。B#fuga
の内部実装に紐付くテストは B#fuga
のユニットテストで行うとして、 A#hoge
の立場から考えると、B#fuga
を意図した引数で意図した回数呼び出せれていることが担保できていればそれで良い。
その際に B#fugaが引数なしで1回だけ呼びされること
という内容を設定したモックオブジェクトを使って、テストを記述することになる。
rspecでモックを使うとき
まずはスタブから
以下のサンプルコードで考えたい。
class Article
def fetch(time: Time.zone.now)
articles = PublishedArticlesService.new.articles(time)
...
end
Article#fetch
と呼ぶと PublishedArticlesService#articles
を介して、指定した時刻の時点で公開済になっている記事のリストが取れるイメージ。
(サンプル用なので、命名や設計には目をつぶっていただけると🙏)
さて、このメソッドのユニットテストを書こうとしたときに、PublishedArticlesService#articles
の内部までテストの対象にはしたくない。そこで、まずこれをスタブにする。rspecでは
it '期待する値が返ってくること' do
allow(PublishedArticlesService).to receive(:new).and_return(service)
allow(service).to receive(:articles).and_return(articles)
is_expected.to eq ...
end
のように定義する。
まず、PublishedArticlesService.new
で、返すオブジェクトをスタブで定義して、そのオブジェクトに対して、articles
で返すオブジェクトをスタブとして定義している。(and_return で返している値は let
で定義されているものとして扱っている)
さて、この記述はit
ブロックの中に書いたが、スタブの定義は実行するテストの前処理として扱いたいのでbefore
ブロックとして書いた方がテスト対象の見通しが良くなる。
before do
allow(PublishedArticlesService).to receive(:new).and_return(service)
allow(service).to receive(:articles).and_return(articles)
end
...
it '期待する値が返ってくること' do
is_expected.to eq ...
end
ここでテストが成功することを確認する。つまりメソッドのI/Fとして、意図する値を入れたときに意図する値が返ってくるかを確認する。
モックに置き換える
そうしたら、スタブにしていたもののうち、引数や呼び出し回数を検証したい部分をモックとして置き換えていく。
今は、#articles
が意図した引数で呼ばれているかを検証したいとして、
allow(service).to receive(:articles).and_return(articles)
から
expect(service).to receive(:articles).with(Time.zone.now).and_return(articles)
と書き直す。Article#fetch
を引数無しで呼び出したときと、引数ありで呼び出したときでは、サービスクラスのarticles
の引数が変わってくるので、それをモックを使ってテストしている。
これでモックを使ったテストにすることができた。
スタブとモックを分けて記述する
これでもテストとしては検証したいことができているし、完成としてもいいのだが、自分はもう1ステップ追加してテストのリファクタリングを行っている。
それは「スタブとモックを分ける」ということだ。
前章までで、テストは以下のようになっている。
before do
allow(PublishedArticlesService).to receive(:new).and_return(service)
expect(service).to receive(:articles).with(Time.zone.now).and_return(articles)
end
...
it '期待する値が返ってくること' do
is_expected.to eq ...
end
モックを導入した際に、もともとテストの前条件を記述していたbeforeブロックに、検証のコードが追加されてしまっている。
検証をするexpectはitブロックの中だけで書きたいと考え、これを実現するのが have_received
を使ったモックの定義である。
before do
allow(PublishedArticlesService).to receive(:new).and_return(service)
allow(service).to receive(:articles).and_return(articles)
end
...
it '期待する値が返ってくること' do
is_expected.to eq ...
end
it '意図した引数で呼び出されていること' do
subject
expect(service).to have_received(:articles).with(Time.zone.now)
end
このように書くことで、スタブはテストの前提条件としてbeforeブロック内に、モックはテスト対象なのでitブロック内にその記述を閉じ込めることができた。
1つ注意点としては、have_received
という単語からも想像つくように、この呼出はテスト対象のメソッドを呼び出した後に検証する必要がある。
反対に、receive
を使ったモックでは対象のメソッドを呼び出す前に検証のコードを記述する必要がある。
まとめ
モックを使って検証したい場合は
- まずスタブを使って、メソッド単体でのI/Fを検証するテストコードを書く
- スタブからモックに置き換える
- スタブとモックに分ける
以上の流れでテストを書いている。
やや冗長な面はあるが、前提条件と検証のコードを分離して記述できるので自分は好んで使っている。
Discussion