Help us understand the problem. What is going on with this article?

使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」

はじめに

みなさんこんにちは!
この記事は「必要最小限の努力で最大限実戦で使える知識を提供するRSpec入門記事」、略して「使えるRSpec入門」の第3回です。

今回はRSpecのモックを使ったテストについて説明します。
これまでモックを全く使ったことがない人でもわかるように丁寧に説明していくつもりです。
また、これまでの回と同様、個人的に使用頻度が低いと思っている内容についてはバッサリ説明を省きます。

ただし、第1回や第2回に比べるとテストコードが少し複雑になって、仕組みや動きを想像するのがちょっと難しいかもしれません。
ぱっと頭に入ってこない場合はじっくり本文を読んだり、実際に自分で写経しながらコードを動かしたりするなどして、少し時間をかけながら理解するようにしてください。

今回は以下のような内容を説明します。

  • モックの基本的な使い方
  • モックを使った検証
  • モックでわざとエラーを発生させる
  • モックを使った厳密な検証(呼び出し回数や引数の検証)
  • allow_any_instance_of
  • receive_message_chain
  • as_null_object

また、最後にモックを使ったテストの注意点も書いているので、一通り最後まで読んでもらうことをお薦めします。

対象となる読者

  • RSpecのモックを一度も使ったことがない人
  • モックを使ったことはあるが、あまり使いこなせていない人

対象となるRSpecとRubyのバージョン

  • RSpec 3.1
  • Ruby 2.1

なお、今回の投稿ではRailsは出てきません。
「素のRubyプログラム」を対象にします

第1回と第2回の記事はもう読みましたか?

第1回では「RSpecの基本的な構文や便利な機能を理解する」というテーマでRSpecの基本を説明しました。
第2回では「使用頻度の高いマッチャを使いこなす」というテーマでマッチャの使い方をいろいろと説明しました。
今回、「読者のみなさんは第1回と第2回の内容は理解できている」という前提で説明していくので、まだ読んでいない方は先に過去の記事を読んでおいてください。

それでは始めましょう!

「で、モックって何ですか?」

はい、まずは「モック」の説明をしておいた方がよいですね。

モックとはざっくりいうと 「本物のふりをするニセモノのプログラム」 のことです。
何らかの理由で本物のプログラムが使えない、もしくは使わない方がよいケースでモックが使われます。

たとえば、外部のAPIを利用しなければならない場合です。
実際、このあとで紹介するサンプルプログラムでもTwitterのAPI(gem)経由でツイートするプログラムを使っています。

モック?スタブ?テストダブル?それとも・・・??

「本物のふりをするニセモノのプログラム」はいろいろな呼ばれ方をします。
Wikipediaの「テストダブル」のページには、以下のような「テストダブルのパターン」が載っています。

  • テストスタブ (テスト対象に「間接的な入力」を提供するために使う。)
  • テストスパイ (テスト対象からの「間接的な出力」を検証するために使う。出力を記録しておくことで、テストコードの実行後に、値を取り出して検証できる。)
  • モックオブジェクト (テスト対象からの「間接的な出力」を検証するために使う。テストコードの実行前に、あらかじめ期待する結果を設定しておく。検証はオブジェクト内部で行われる。)
  • フェイクオブジェクト (実際のオブジェクトに近い働きをするが、より単純な実装を使う。例として、実際のデータベースを置き換えるインメモリデータベースが挙げられる。)
  • ダミーオブジェクト (テスト対象のメソッドがパラメータを必要としているが、そのパラメータが利用されない場合に渡すオブジェクト。)

・・・がどれが何なのか、僕もぱっと用語を切り替えることができません(苦笑)。

人によっては用語の定義が気になって「それはモックだ」「いや、スタブでしょう」「テストダブルなんじゃない?」と考える人がいるかもしれません。
ですが、ややこしいのでこの記事では「本物のふりをするニセモノのプログラム(オブジェクトやメソッド)」をまとめて「モック」と呼ぶことにします。

一番単純なモックの使い方

それでは簡単なサンプルプログラムを使って、モックの使い方の基本を理解していきましょう。

こんなRubyプログラムがあったとします。
これは天気予報をツイートする架空のBotプログラムです。

# 注:本当に動かす場合はtwitter gemが必要です
require 'twitter'

class WeatherBot
  def tweet_forecast
    twitter_client.update '今日は晴れです'
  end

  def twitter_client
    Twitter::REST::Client.new
  end
end

何も考えずにテストを書くとこうなります。

it 'エラーなく予報をツイートすること' do
  weather_bot = WeatherBot.new
  expect{ weather_bot.tweet_forecast }.not_to raise_error
end

しかしこのままでは本当に全世界へ向けてツイートを発信することになります。

実際にツイートしてしまうとテスト中の間抜けなメッセージが公開されてしまうだけでなく、他にも様々な問題を引き起こします。
たとえば、同じツイートを連続して投稿するとTwitterの仕様上エラーになるし、短時間に何度も実行するとレートリミットエラーが起きるし、Twitter側の調子が悪くて突然エラーが起きるかもしれないし、外部との通信が発生するのでテストが遅くなってしまうし、などなど・・・。

というわけで、Twitterと連携する部分はモックに置き換えて、テスト中にTwitterとの通信が発生しないようにしてみましょう。

モックを使ったテストコードはこのようになります。

it 'エラーなく予報をツイートすること' do
  # Twitter clientのモックを作る
  twitter_client_mock = double('Twitter client')
  # updateメソッドが呼びだせるようにする
  allow(twitter_client_mock).to receive(:update)

  weather_bot = WeatherBot.new
  # twitter_clientメソッドが呼ばれたら上で作ったモックを返すように実装を書き換える
  allow(weather_bot).to receive(:twitter_client).and_return(twitter_client_mock)

  expect{ weather_bot.tweet_forecast }.not_to raise_error
end

ちょっとややこしいので、順を追って説明していきます。

1. 空のモックオブジェクトを作る

最初に Twitter::REST::Client のニセモノ、つまりモックを作ります。

twitter_client_mock = double('Twitter client')

double というメソッドを使うと、モックオブジェクトを作れます。
引数で渡す文字列は任意です。好きな文字列を渡しても構わないですし、省略することもできます。
ただし、この文字列はテスト失敗時のメッセージに表示されるので、できるだけわかりやすい名前を付けておく方がいいでしょう。

以下はテスト失敗時のメッセージ例です。
"Twitter client" の部分が double に渡した引数です。

RSpec::Mocks::MockExpectationError: Double "Twitter client" received unexpected message :update with ("今日は晴れです")

ちなみに、英語の「double」には「影武者」や「代役」といった意味があります。

2. モックに返事の仕方を教え込む

double で作られたモックは何も知らない生まれたての赤ちゃんです。
なので、周りの大人から声をかけられたらどう答えればいいのか、赤ちゃんに教えてやる必要があります。

もう一度サンプルコード(アプリケーション側)を見てみましょう。

def tweet_forecast
  twitter_client.update '今日は晴れです'
end

def twitter_client
  Twitter::REST::Client.new
end

tweet_forecast メソッドが呼ばれると、メソッド内で twitter_client.update を呼び出しています。
twitter_client の部分が今回モックに変わる予定なので、モックは update というメソッドを呼び出せるようにする必要があります。

その設定をしているのが次のコード(テスト側)です。

allow(twitter_client_mock).to receive(:update)

RSpecでは allow(モックオブジェクト).to receive(メソッド名) の形で、モックに呼び出し可能なメソッドを設定できます。
ここでは何も知らない赤ちゃん( twitter_client_mock )に、「きみは update と声をかけられるかもしれないよ」ということを教え込みました。

また、この状態では update 以外のメソッドを呼び出すことはできません。
もし呼び出されるとエラーが発生し、テストが失敗します。
他のメソッドも呼ばれる予定になっている場合は、同じ構文を使って呼び出し可能なメソッドを追加していく必要があります。

ちなみに英語的には "allow A to receive B" で、「A が B を受け取ることを許可する」の意味になります。

3. アプリケーションコードにモックをこっそり送り込む

さて、このままではアプリケーションコードとモックはお互い赤の他人です。
テストを実行するとアプリケーションコードは相変わらず全世界に向けて間抜けなメッセージをツイートしてしまいます。

そこでアプリケーション側の実装をこっそりモックに置き換えます。
それをやっているのが以下のコード(テスト側)です。

weather_bot = WeatherBot.new
allow(weather_bot).to receive(:twitter_client).and_return(twitter_client_mock)

念のため、もう一度アプリケーション側のコードも載せておきます。

def tweet_forecast
  twitter_client.update '今日は晴れです'
end

def twitter_client
  Twitter::REST::Client.new
end

テストコードでやったのは、アプリケーション側の twitter_client メソッドの置き換えです。
つまり、 twitter_client メソッドを呼び出されたら、 Twitter::REST::Client.new ではなく、 twitter_client_mock を返すように変更しました。

構文としては「2. モックに返事の仕方を教え込む」で見せたものとほぼ同じです。

allow(実装を置き換えたいオブジェクト).to receive(置き換えたいメソッド名).and_return(返却したい値やオブジェクト)

というわけで、 weather_bot くんはテスト実行中に改造手術を受け、 twitter_client メソッドを呼び出されたときに本来の実装とは異なるオブジェクト( twitter_client_mock )を返すようになりました。

4. テストしたいメソッドを呼び出す

これで準備万端です。
モック( twitter_client_mock )には update というメソッドが呼ばれることを教え、アプリケーション側( weather_bot )は本来の実装を置き換えられてモックを使うようになりました。

あとは何事もなかったようにアプリケーション側のメソッド( tweet_forecast )を呼び出せばOKです。

expect{ weather_bot.tweet_forecast }.not_to raise_error

weather_bot はモックを使うように改造されているので、 twitter_client.update '今日は晴れです' を実行しても実際にツイートが飛ぶことはありません。

復習のためにもう一度テストコードの全体像を載せておきます。
理解できていない箇所があればもう一度上の説明を読み直してください。

it 'エラーなく予報をツイートすること' do
  twitter_client_mock = double('Twitter client')
  allow(twitter_client_mock).to receive(:update)

  weather_bot = WeatherBot.new
  allow(weather_bot).to receive(:twitter_client).and_return(twitter_client_mock)

  expect{ weather_bot.tweet_forecast }.not_to raise_error
end

モックのメソッドがちゃんと呼び出されることを検証する

ところで、上の例で作ったモックは「 update メソッドを呼び出すことができるだけ」でした。
しかし、それに加えてモックには「セットアップしたメソッドがちゃんと呼びされたかどうか」を検証する機能があります。

たとえば、さっきのテストコードを少し改造して、「update メソッドが呼び出されること」も検証してみましょう。

it 'エラーなく予報をツイートすること' do
  twitter_client_mock = double('Twitter client')
  # updateメソッドが呼ばれることもあわせて検証する
  expect(twitter_client_mock).to receive(:update)

  weather_bot = WeatherBot.new
  allow(weather_bot).to receive(:twitter_client).and_return(twitter_client_mock)

  expect{ weather_bot.tweet_forecast }.not_to raise_error
end

実はテストコードはほとんど変わっていません。
変更したのはこの一行だけです。

expect(twitter_client_mock).to receive(:update)

先ほどは allow で始まっていましたが、ここでは expect に変わっています。

allow を使って呼び出し可能なメソッドをセットアップしたときは「呼び出されようと、呼び出されまいと知らんぷり」です。
一方、 expect を使うと「そのメソッドが呼び出されないとテスト失敗」になります。

モックを使うときは、単に実装を置き換えたいだけなのか、それともメソッドの呼び出しも検証したいのかに応じて、 allowexpect を使い分ける必要があります。

さらにモックを使いこなす

さて、ここまでの内容を理解できればモックの基本はマスターしたことになります。
あとは必要に応じて、さらに便利なモックの機能を使いこなせるようになれば完璧です。

というわけで、ここからはモックを使った応用テクニックをいくつか紹介します。

わざとエラーを発生させてエラー処理をテストする

先ほどのサンプルコードにエラー処理を追加してみましょう。
ツイートを発信するときにエラーが発生したら、そのエラーを通知するようにしてみます。

class WeatherBot
  def tweet_forecast
    twitter_client.update '今日は晴れです'
  rescue => e
    notify(e)
  end

  def twitter_client
    Twitter::REST::Client.new
  end

  def notify(error)
    # (エラーの通知を行う。実装は省略)
  end
end

次にテストコードを書いてみます。
ここでは「エラーが起きたら notify メソッドが呼ばれること」を検証しています。

it 'エラーが起きたら通知すること' do
  twitter_client_mock = double('Twitter client')
  # updateメソッドが呼ばれたらエラーを発生させる
  allow(twitter_client_mock).to receive(:update).and_raise('エラーが発生しました')

  weather_bot = WeatherBot.new
  allow(weather_bot).to receive(:twitter_client).and_return(twitter_client_mock)
  # notifyメソッドが呼ばれることを検証する
  expect(weather_bot).to receive(:notify)

  # tweet_forecastメソッドを呼び出す
  # weather_botのnotifyメソッドが呼び出されたらテストはパスする
  weather_bot.tweet_forecast
end

まず注目したいのはこのコードです。

allow(twitter_client_mock).to receive(:update).and_raise('エラーが発生しました')

最初のテストコードでは twitter_client_mockupdate メソッドを呼び出されても何もしませんでしたが、ここではエラーを発生させるようにしました。

コードを見ると予想が付くかもしれませんが、 allow(オブジェクト).to receive(メソッド名).and_raise(エラー) の形でメソッドが呼ばれたときに、人為的にエラーを発生させることができます。

それから、次に注目したいのはこのコードです。

expect(weather_bot).to receive(:notify)

エラー処理が適切に行われれば weather_botnotify メソッドが呼ばれるはずです。
そこで notify メソッドをモック化して、このメソッドがちゃんと呼ばれることを検証しています。

最後に weather_bot.tweet_forecast を呼びだして、アプリケーションコードを実行しています。

ちなみに、最初に見せたテストコードと同様に expect{ weather_bot.tweet_forecast }.not_to raise_error と書いても構わないのですが、ここではあくまで「通知が行われるかどうか」を検証したいだけなので、余計なエクスペクテーションは除外しました。

余談:エラー処理こそテストが大事
Twitterのような外部APIを利用するときにかかわらず、エラー処理のテストはまずエラーをわざと発生させること自体が難しいケースが多いです。

テストを自動化していないと手作業で動作確認する必要がありますが、エラー処理だけはそういった理由から動作確認できていない、ということがよくあります。
そのため、本番環境でいざエラーが起きたというときに、エラー処理そのものに不具合があって適切なエラー処理が行われず、エラーの真相が闇に葬られる・・・なんていう場面に僕は何度か出くわしたことがあります。

それだけに、エラー処理のテストを自動化することは非常に重要です。
そして、エラーを発生させることが難しい場合はうまくモックを使ってあげましょう。

2015.09.29 追記:エラー処理の考え方をまとめました

以下のQiita記事にてエラー処理の考え方をまとめました。
モックを使ったエラー処理のテストも紹介しています。
こちらもあわせてご覧ください。

Railsアプリケーションにおけるエラー処理(例外設計)の考え方 - Qiita

メソッドの呼び出し回数を検証する

expect(オブジェクト).to receive(メソッド名) で、そのメソッドが呼び出されたことを検証できるのは前に説明したとおりです。

これをもっと厳密に検証したい場合は、次のようにしてメソッドの呼び出し回数も検証できます。
たとえばこんな感じです。

expect(weather_bot).to receive(:notify).once

上のコードのように once を付けると「1回だけ呼び出されること」を検証できます。
(2回以上呼び出されるとエラーになってテストが失敗します)

呼び出し回数を検証する場合は、他にも次のようなバリエーションがあります。

メソッド名 検証内容
once 1 回だけ
twice きっちり 2 回
exactly(n).times きっちり n 回
at_least(:once) 1 回以上(デフォルト)
at_least(:twice) 2 回以上
at_least(n).times n 回以上
at_most(:once) 0 または 1 回
at_most(:twice) 2 回以下
at_most(n).times n 回以下

詳細は公式ドキュメントをご覧ください。

Receive Counts

引数の内容を検証する

メソッドの呼び出し回数だけでなく、引数の中身を検証することもできます。

たとえば、「update メソッドは引数として必ず "今日は晴れです" を受け取ること」を検証したい場合は次のように書きます。

expect(twitter_client_mock).to receive(:update).with('今日は晴れです')

ご覧のように with(検証したい引数の内容) の形で引数の中身を検証できます。
テストの実行中に "今日は晴れです" 以外の引数が渡ってきた場合はエラーが発生し、テストが失敗します。

また、先ほど説明した呼び出し回数の検証と組み合わせることも可能です。

# update メソッドが呼ばれることを検証する
# ただし、引数は '今日は晴れです' かつ、呼び出される回数は1回だけであること
expect(twitter_client_mock).to receive(:update).with('今日は晴れです').once

引数を検証する場合は他にもいくつかバリエーションがあります。
(サンプルコードは WeatherBot とは無関係の架空のコードです)

2つ以上の引数を受け取る場合

引数が複数ある場合はカンマ区切りで期待する引数を並べます。

expect(user).to receive(:save_profile).with('Alice', 'alice@example.com')

どんな引数でも良い場合

検証不要な引数がある場合は anything を使います。

expect(user).to receive(:save_profile).with('Alice', anything)

ハッシュを引数として渡す場合

厳密にハッシュの内容を検証する場合はそのままハッシュ(keyとvalue)を with に渡します。

expect(user).to receive(:save_profile).with(name: 'Alice', email: 'alice@example.com')

ハッシュ全体ではなく、特定のkeyとvalueだけに着目する場合は hash_including を使います。

expect(user).to receive(:save_profile).with(hash_including(name: 'Alice'))

その他

他にも引数を検証する方法はありますが、個人的にあまり使っていないのでこれ以上の説明は割愛します。
気になる方は公式ドキュメントの内容を確認してみてください。

Matching arguments

もっとマニアックなモックの使い方

さて、ここまでのテクニックを使いこなせれば、モックを使う上で困るケースはほとんどないと思います。
実際、僕もモックを使う時はここまでに説明した内容で9割以上事足りてます。

しかし、これ以外のテクニックもたまに使います。
あまり深追いはしませんが、そうしたテクニックについてもざっくりと説明しておきます。

どうしてもモックを挟み込むことができない場合:allow_any_instance_of

モックを使う場合は必ずどこかでモックと実際の実装を置き換える必要があります。
純粋な単体テストであれば置き換えやすいですが、複雑なテストや結合テストでは置き換えるのが難しかったり、不可能だったりする場合があります。
(ありがちなのはRailsのフィーチャスペックです)

そんな場合は allow_any_instance_of メソッドを使うと、対象クラスの全インスタンスに対して目的のメソッドをモック化できます。

あまり実践的な例ではありませんが、 allow_any_instance_of を使って twitter_client メソッドの戻り値をモックと置き換えてみます。

it 'エラーなく予報をツイートすること' do
  twitter_client_mock = double('Twitter client')
  allow(twitter_client_mock).to receive(:update)

  # WeatherBotクラスの全インスタンスに対して、twitter_clientメソッドが呼ばれたときにモックを返すようにする
  allow_any_instance_of(WeatherBot).to receive(:twitter_client).and_return(twitter_client_mock)

  weather_bot = WeatherBot.new
  expect{ weather_bot.tweet_forecast }.not_to raise_error
end

ご覧のように allow_any_instance_of(クラス名).to receive(メソッド名).and_return(戻り値) の形で、全インスタンスの挙動を変更することができました。

使用上の注意

ただし、RSpecの公式ドキュメントによると「allow_any_instance_of はあまり使わない方がよい」と書いてあります。
理由を簡単にまとめると以下の通りです。

  • モックのAPIは個々のインスタンスに対して作用することを前提にしているので、予期しない挙動や理解しづらいテストコードの原因になる。
  • この機能が必要になるということは、テストしようとしているアプリケーションの設計自体がイケてない可能性がある。
  • allow_any_instance_of の中身は複雑な実装になっていて昔から不具合が多い。

というわけで、安易に allow_any_instance_of には手を出さず、どうしても必要な場合だけに限定するのがよいと思います。

モックを何個も作って連結する場合:receive_message_chain

ここまでは比較的シンプルなテストコードを見てきましたが、実務で書くテストコードはもっと複雑なものになっていることがあります。
特に、戻り値を返すモックの場合、モックがモックの子を返し、モックの子がモックの孫を返し・・・と、いくつもモックを返さなければならない場合があります。

たとえば、これまで見てきたサンプルコードを少し改造して、新しいメソッド search_first_weather_tweet を追加してみます。

class WeatherBot
  def search_first_weather_tweet
    twitter_client.search('天気').first.text
  end

  def tweet_forecast
    # (省略)
  end

  def twitter_client
    Twitter::REST::Client.new
  end
end

search_first_weather_tweet は '天気' というキーワードでTwitterを検索し、最初に返ってきたツイートの本文を返すメソッドです。

このメソッドをテストする場合、ここまでに学んだ知識を使うと次のようなコードになります。

it '「天気」を含むツイートを返すこと' do
  status_mock = double('Status')
  allow(status_mock).to receive(:text).and_return('西脇市の天気は曇りです')

  twitter_client_mock = double('Twitter client')
  allow(twitter_client_mock).to receive(:search).and_return([status_mock])

  weather_bot = WeatherBot.new
  allow(weather_bot).to receive(:twitter_client).and_return(twitter_client_mock)

  expect(weather_bot.search_first_weather_tweet).to eq '西脇市の天気は曇りです'
end

「検索実行」と「本文の返却」という2つの処理をモック化するので、 Twitter::REST::Client だけでなく、 search メソッドの戻り値(Status)もモック化する必要があります。
なので、 allow(twitter_client_mock).to receive(:search).and_return([status_mock]) のように、モックがモックを返すようなセットアップも必要になります。

このような場合は receive_message_chain を使うと短く書ける場合があります。
上のテストコードを receive_message_chain を使って書き直してみましょう。

it '「天気」を含むツイートを返すこと' do
  weather_bot = WeatherBot.new
  allow(weather_bot).to receive_message_chain('twitter_client.search.first.text').and_return('西脇市の天気は曇りです')

  expect(weather_bot.search_first_weather_tweet).to eq '西脇市の天気は曇りです'
end

少し横に長いですが、 receive_message_chain('twitter_client.search.first.text') の部分がポイントです。
ここでは 「twitter_client => search => first => text」と4つのメソッドを呼び出した結果を一気にモック化しています。
そして、 and_return('西脇市の天気は曇りです') で最後の text メソッドが返す戻り値を定義しました。

この結果、アプリケーションコード側の search_first_weather_tweet メソッドはモックを経由して '西脇市の天気は曇りです' という文字列を返すようになりました。

使用上の注意

ただし、この機能もRSpecの公式ドキュメントでは利用する際には注意が必要と書いてあります。
その理由は receive_message_chain を使っているということは、「デメテルの法則」に違反したクラス設計になっているかもしれないから、というものです。

詳しくはRSpecの公式ドキュメントを読んでみてください。

Message Chains

デメテルの法則については @hirokidaichi さんによる以下のQiita記事に説明があります。

何かのときにすっと出したい、プログラミングに関する法則・原則一覧

どんなメソッドが呼ばれても許容する場合:as_null_object

デフォルトのモックは厳密にメソッドの呼び出しをチェックします。
厳密なので事前にセットアップしていないメソッドが呼ばれるとエラーが発生します。

しかし、 as_null_object というメソッドを付けると、どんなメソッドが呼ばれても許容するように性格が変わります。

it 'エラーなく予報をツイートすること' do
  # null object としてモックを作成する
  twitter_client_mock = double('Twitter client').as_null_object
  # allow(twitter_client_mock).to receive(:update) は不要

  weather_bot = WeatherBot.new
  allow(weather_bot).to receive(:twitter_client).and_return(twitter_client_mock)

  expect{ weather_bot.tweet_forecast }.not_to raise_error
end

ご覧の通り、 as_null_object を付けたので、「update の呼び出しに答えるべし」というセットアップが不要になりました。

ちなみに、 null object はメソッドの戻り値として自分自身、つまり null object を返します。
なので以下のようなコードを書いてもエラーになりません。

twitter_client_mock = double('Twitter client').as_null_object
# 子、孫、ひ孫・・・と永遠にメソッドの呼び出しが可能
twitter_client_mock.foo.bar.hoge.piyo

モックの使われ方が変わったときにすぐ気づけるよう、null object は基本的に使わない方がよいと思いますが、サンドボックス的な仮のテストコードを書いたりするときには便利かもしれません。

もっともっとマニアックな使い方!?

RSpec Mocksには僕も使ったことがないような機能が他にもたくさんあります。
気になる方は公式ドキュメントを一通り読んでみてください。

RSpec Mocks 3.1

【重要】モックを使う場合の注意点

さて、ここまでモックの使い方をいろいろ説明してきましたが、そもそもモックを使う場合に注意しなければいけないことがいくつかあります。

注意点1:モックが使いやすい設計にしましょう

最初に紹介したサンプルコードをもう一度載せてみます。

class WeatherBot
  def tweet_forecast
    twitter_client.update '今日は晴れです'
  end

  def twitter_client
    Twitter::REST::Client.new
  end
end

何ともないコードに見えますが、実はモックを使うことを前提にして Twitter::REST::Client.new を別メソッドに分けています。

もしこのコードがこんなふうになっていると、モックを挟み込むのが難しくなります。

class WeatherBot
  def tweet_forecast
    twitter_client = Twitter::REST::Client.new
    twitter_client.update '今日は晴れです'
  end
end

特に、外部APIを利用するコードはテストを考慮して最初から別クラスや別メソッドに分離させておくのが良いと思います。
(分離しておくとテスト容易性の面だけでなく、保守性や可読性の面でも良い効果が期待できます。)

注意点2:モックで動いても本物のコードで動くとは限りません

当たり前ですが、モックはあくまでモックです。
すなわち「本物のふりをするニセモノ」です。

モックを使ったテストがパスしているからといって、本番環境でそのコードが絶対に動くという保証はありません。
なので、テストがパスしたあとも手作業で動作確認するなどして、本物のコードもちゃんと動くことを確認しておきましょう。

また、モックを多用しすぎると「単なるモックのテストになっていた」という事態が起こりえます。
実際、 receive_message_chain の使い方で説明したテストコードはほとんどモックを動かしているだけに過ぎません。
こうしたテストに全く意味がないとは言いませんが、モックを使う前に「モックを使うことで何がテストできて、何がテストできないのか」を意識する必要があります。

まとめ

というわけで今回はRSpecのモックの使い方を説明していきました。

モックの使用頻度はそこまで高いわけではありませんが、使い方を知っていると「ここのテストを書くのは無理なんじゃない?」と思うようなコードもテストできたりします。
「ここぞ」という場面でうまくモックを活用できるようになっておきましょう。
ただし、くれぐれも濫用や過信は禁物ですよ。

さて、この入門記事シリーズはあともう一本公開する予定です。
第4回では「CapybaraのDSL」を取り上げるつもりです。

「続きが読みたい!」という方はこの記事をストックしてもらうと、次の記事を投稿したときに通知メールを送りますので、どうぞよろしくお願いします!

【2015.01.02 追記】第4回の記事を公開しました!

あわせて読みたい

RSpecの公式ドキュメント

今回紹介した内容は、以下の公式ドキュメントを僕なりに要約したような内容になっています。
詳しい仕様を確認したい方は公式ドキュメントを読んでみてください。

RSpec Mocks 3.1

TDDのプロセスを重視したRSpecの入門記事

また、RSpec関連の入門記事としてはこちらの記事もオススメです。

「RSpecの入門とその一歩先へ ~RSpec 3バージョン~」

PR: RSpec 3.1に対応した「Everyday Rails - RSpecによるRailsテスト入門」が発売中です

僕が翻訳者として携わった 「Everyday Rails - RSpecによるRailsテスト入門」 という電子書籍が発売中です。

Railsアプリケーションを開発している方で、実践的なテストの書き方を学んでみたいという人には最適な一冊です。
現行バージョンはRSpec 3.1に対応しています。
一度購入すれば将来無料でアップデート版をダウンロードできるのも本書の特徴の一つです。
よかったらぜひ読んでみてください!

Everyday Rails - RSpecによるRailsテスト入門
Everyday Rails

本書の内容に関する詳しい情報はこちらのブログをどうぞ。

RSpec 3.1に完全対応!「Everyday Rails - RSpecによるRailsテスト入門」をアップデートしました

jnchito
SIer、社内SEを経て、ソニックガーデンに合流したプログラマ。 「プロを目指す人のためのRuby入門」の著者。 http://gihyo.jp/book/2017/978-4-7741-9397-7 および「Everyday Rails - RSpecによるRailsテスト入門」の翻訳者。 https://leanpub.com/everydayrailsrspec-jp
https://blog.jnito.com/
sonicgarden
「お客様に無駄遣いをさせない受託開発」と「習慣を変えるソフトウェアのサービス」に取り組んでいるソフトウェア企業
http://www.sonicgarden.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした