Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

RSpecのモック(Mock)の使い方を徹底解説!ステップバイステップチュートリアル

RSpecのモック(Mock)とは?

まずは基本から確認しましょう。「モック」とは何でしょうか?(これはRSpec固有の概念ではなく、ソフトウェアテスト全般で使われる考え方です。)

モックとは、テストのために使う「代役オブジェクト」のことです。

通常のテストではメソッドの戻り値を検証しますが、モックを使うと「2つのオブジェクト間のやり取り(相互作用)」をテストできます。

具体例を挙げます

画像を反転させるAPIを開発しているとしましょう。

画像処理のコードを自前で書くのは大変なので、mini_magickのようなGemを利用することにしました。

ここで、自分のコードとこの外部依存関係とのやり取りをテストしたい場合、ImageProcessorクラスに対して「正しいメソッドが呼び出されること」を期待するモックを作成します。

こうすることで、テストを実行するたびに実際の画像反転処理(時間のかかる操作)を行わずに済みます。

仕組みはどうなっているのでしょうか?

モックは元のオブジェクトを置き換えるため、本物のメソッドは呼び出されません。

それでは、コード例を見ていきましょう!

RSpecモックの実装例

ImageFlipperのテストコードは次のとおりです:

RSpec.describe "ImageFlipper" do
  it "calls the flip method with the correct arguments" do
    mock = double("mini_magick")
    expect(mock).to receive(:flip).with("ruby.jpg")

    img = ImageFlipper.new(mock)
    img.flip("ruby.jpg")
  end
end

このテストを使えば、TDD(テスト駆動開発)の流れでコードを書き進められます。

まずImageFlipperクラスを作成する必要があります。

次のように書きます

class ImageFlipper
  def initialize(image_processor)
    @image_processor = image_processor
  end
end

さらに flip メソッドも追加しましょう:

def flip(file_name)
end

この状態でRSpecを実行すると、次のようなフィードバックが返ってきます:

Failures:

1) ImageFlipper calls the flip method with the correct arguments
   Failure/Error: expect(mock).to receive(:flip).with("ruby.jpg")

     (Double "mini_magick").flip("ruby.jpg")
         expected: 1 time with arguments: ("ruby.jpg")
         received: 0 times
   # ./rspec-mocks.rb:6:in `block (2 levels) in <top (required)>'

これは「flipメソッドが0回しか呼び出されなかったが、1回呼ばれることを期待していた」という意味です。

RSpecが求めていることを実装してあげると、テストをパスできます:

def flip(file_name)
  @image_processor.flip(file_name)
end

これで無事テストが通りました

.

Finished in 0.00751 seconds (files took 0.11157 seconds to load)
1 example, 0 failures

振り返り:何をしたのか?

私たちは、image_processorを受け取る ImageFlipper クラスを作成しました。このプロセッサは flip メソッドに応答します。そしてモックを使い、「このメソッドが指定した引数付きでちょうど1回呼び出されたか」を検証しました。

確かに、この例はとてもシンプルです。しかし、ファイルの存在確認や画像形式の妥当性チェックなどを行う、より完全な ImageFlipper の実装でも同じ要領でテストできることを想像してみてください。

モックと値テストの違い

通常のテストではメソッドの戻り値をチェックします:

「このメソッドは反転済みの画像を返したか?」

一方、モックを使う場合は振る舞い(behavior)をテストします:

「相手に対して、正しい情報で、必要な回数ぴったりだけ指示を出せたか?」

モックとスタブ(Stub)の違い

もう一つ混乱しやすいのが、モックとスタブの比較です。どこが違うのでしょうか?

  • スタブ:あらかじめ用意した固定の応答を返すだけのメソッド。呼び出されたかどうかは関知しません。
  • モック:メソッドが呼び出されることを期待します。呼び出されなければテストは失敗します。

RSpecでのスタブの例

stub = double("json")

allow(stub).to receive(:response) do
  {"blog"=>"rubyguides.com", "rating"=>"5/5"}.to_json
end

allow メソッドを使っている点がスタブの特徴です。テスト用の double("json") オブジェクトに対して、このメソッドを受け取って応答することを「許可」していますが、実際に呼び出されたかどうかの検証は行いません

これが両者の決定的な違いです!

ベリファイドダブル(Verified Doubles)の使い方

モックやスタブには欠点もあります。プロダクションコードに存在しないメソッドを使ってしまう可能性があるのです。メソッド名が変更されていたり、単純にタイポ(打ち間違い)があったりすることもあるでしょう。

そこで登場するのがベリファイドダブル(verified double)です。

ベリファイドダブルはスタブ(allow)としてもモック(expect)としても利用でき、しかも指定した名前のメソッドが実際に存在するかどうかをチェックしてくれます。

mock = instance_double(ImageProcessor)

メソッドが存在しない場合、次のようにエラーが発生します:

1) ImageFlipper calls the flip method with the correct arguments
     Failure/Error: expect(mock).to receive(:flip).with("ruby.jpg")
       the ImageProcessor class does not implement the instance method: flip

逆にメソッドが存在していれば、問題なく動作します。

モックの戻り値を設定する方法

再びモックの話に戻りましょう。

先ほどのでは、次のようなコードを使いました:

expect(mock).to receive(:flip).with("ruby.jpg")

コードが flip を呼び出すと、モックはデフォルトで nil を返します。もしコード側が nil 以外の値を期待していると、エラーになってしまいます。

これを解決するには、モックに戻り値を設定します。

次のように書きます

expect(mock).to receive(:flip).with("ruby.jpg").and_return("ruby-flipped.jpg")

インスタンスメソッドをモックする方法

次のようなコードがあるとします:

class NumberGenerator
  def random
    "A" * rand(1..10)
  end
end

このメソッドはランダム性があるため、そのままではテストが困難です。

RSpecでは rand をモックまたはスタブできます。

次のように書きます

it "generates a random number" do
  generator = NumberGenerator.new

  allow(generator).to receive(:rand).and_return(5)

  expect(generator.random).to eq("AAAAA")
end

これでrand が固定値を返すようになるため、メソッドの出力結果を安定してテストできます。

理想的には、依存関係(この場合は rand 関数)を依存性注入(DI)によって渡せるように設計し、テスト側から制御できるようにするのが望ましいでしょう。依存性注入とは、依存オブジェクトを引数として渡すだけのことで、特別に難しい仕組みではありません。

ただし、状況によっては単純にメソッドをスタブ化する方が手軽なこともあります。

モックはどんなときに使うべきか?

さて、ここで最も重要な疑問が浮上します……

一体、いつモックを使うべきなのでしょうか?

ソフトウェア開発は複雑な領域ですが、目安となるガイドラインがあります:

  1. テスト対象のメソッドが値を返し、副作用(ファイルの作成、APIリクエストなど)を持たない場合は、モックは不要です。単に戻り値をチェックしましょう。
  2. メソッドが外部オブジェクトと連携し、それらに指示を送るような場合は、これらのオブジェクトとのやり取りをモックできます。
  3. メソッドが外部サービス(APIなど)からデータを取得している場合は、スタブを使ってテスト用のデータを提供するとよいでしょう。

ポイントは、モックを「外部とのやり取り」のためだけに使うことです。

言い換えれば……

自分のアプリケーション内のクラスをモックするのは避けましょう!

なぜでしょうか?

テストが実装の詳細と結合してしまい、コードの変更が難しくなるからです。

唯一の例外は、サードパーティ製ライブラリのラッパーとなっているクラスです。

動画でさらに詳しく学びたい方はこちら

まとめ

この記事では、RSpecのモック、スタブ、そしてベリファイドダブルについて学びました!

ぜひこの記事をシェアして、より多くの方にこのコンテンツを届けてください。

最後までお読みいただきありがとうございました🙂


  1. Rubyのmapメソッド完全ガイド!配列・ハッシュのデータ変換を実例で解説

    Rubyのmapメソッドは、配列(Array)・ハッシュ(Hash)・範囲(Range)といったコレクションに対して使用できる強力なメソッドです。 mapの主な用途は、データの変換です。 例えば、文字列の配列があった場合、すべての文字列を順番に処理して、各文字を大文字に変換することができます。 また、Userオブジェクトのリストがある場合も同様です。 それらを変換して、対応するメールアドレスや電話番号など、Userクラスに定義された任意の属性のリストを作成できます。 それでは、具体的な使い方を見ていきましょう! Ruby mapメソッドの基本構文 mapの構文は次のようになっています。 a

  2. Rubyスレッドの使い方を徹底解説!初心者向けわかりやすいチュートリアル

    Rubyにおけるスレッド(Thread)とは、プログラムの中で複数の処理を同時に実行するための仕組みです。スレッドを活用すれば、Rubyプログラムは複数のタスクを並行してこなせるようになり、全体の処理速度向上が期待できます。 具体的には、次のような場面で役立ちます。 複数のファイルを同時に読み込む 複数のWebリクエストを同時に処理する 複数のAPIへ同時に接続する スレッドを使ったプログラムは「マルチスレッド」と呼ばれ、作業をより速く完了できるようになります。 ただし、注意点がひとつあります。 MRI(Matzs Ruby Interpreter)と呼ばれる標準のRuby処理系では、I/