おもしろwebサービス開発日記

Ruby や Rails を中心に、web技術について書いています

minitest で mock や stub を使う

minitest には標準で mock や stub の機能が付いています。それらの挙動について学んだのでメモ。

コード例

下記のような Person クラスと Whisky クラスがあるとします。これらについて minitest の mock と stub を使ってテストを書いてみます。

class Person
  def eat(food)
    food.taste
  end

  def drink(whisky)
    whisky.alcohol.upcase
  end
end

class Whisky
  def alcohol
    # まだ実装されていない
  end
end

mock

minitest では下記のように mock を書きます。

describe Person do
  subject { Person.new }

  describe '#eat' do
    it '引数にとったオブジェクトの #taste を実行していること' do
      food = MiniTest::Mock.new.expect(:taste, 'terrible')
      subject.eat(food)
      food.verify.must_equal true
    end

    it '引数にとったオブジェクトの #smell を実行していないこと' do
      food = MiniTest::Mock.new.expect(:smell, 'good')
      def food.taste; 'terrible' end
      subject.eat(food)
      -> { food.verify }.must_raise(MockExpectationError)
    end
  end
end

mock は MiniTest::Mock のインスタンス。#expect(メソッド名, 戻り値) でチェックしたいメソッドを設定し、 #verify でそのメソッドが実行されたかを調べます。実行された場合は true, 実行されてない場合は MockExpectationError の例外が発生します。

stub

stub は下記のような感じです。RSpec と同じ感じで使っていたらハマりました…><

describe Person do
  subject { Person.new }

  describe '#drink' do
    describe '引数に取ったオブジェクトの #alcohol メソッドが "strong!" を返すとき' do
      it 'STRONG!を返すこと' do
        whisky = Whisky.new
        whisky.stub(:alcohol, 'strong!') do
          subject.drink(whisky).must_equal 'STRONG!'
        end
      end

      it '引数の#alcohol メソッドを呼んでいること' do
        mock = MiniTest::Mock.new.expect(:call, 'strong!')
        whisky = Whisky.new
        whisky.stub(:alcohol, mock) do
          subject.drink(whisky)
        end
        mock.verify.must_equal true
      end
    end
  end
end

minitest の stub メソッドは、「既存のメソッドを一時的に差し替える」用途で使います。stub のブロック中だけ引数で指定したメソッドの戻り値が変わります。 Rspec の stub メソッドと違って、「レシーバとなるオブジェクトにまだ定義されていないメソッド」を stub の引数として指定するとエラーになってしまいます。

「レシーバとなるオブジェクトにまだ定義されていないメソッド」を stub にしたい場合は、stub メソッドを使わず、 mock の方のコード例で書いたように def food.taste; 'terrible' end とその場でメソッドを定義する方法があります。

既存のオブジェクトを mock にしたい場合は、二つ目のテストのように mock に call メソッドを定義し、それを stub の戻り値とすると良いです。stub の戻り値には lambda などの #call を持つオブジェクトを指定することもできます。既存のメソッドを一時的に差し替える必要が無ければ、下記のようにしても良いと思います。

it '引数の#alcohol メソッドを呼んでいること' do
  mock = MiniTest::Mock.new.expect(:call, 'strong!')
  whisky = Whisky.new
  whisky.define_singleton_method(:alcohol) do
    mock.call
  end
  subject.drink(whisky)
  mock.verify.must_equal true
end

感想

minitest の mock と stub、必要十分な機能を備えていると思います。ただゆるふわ RSpec 育ちだと最初少しつまづくかもしれません。