minitest で stub, mock を使う

仕事のプロジェクトでminitestを使っている。
rspecと比較してstubの使い勝手がちょっと違うので適当にまとめておく。

  • 目次
    • ::stub
      • 特定のオブジェクトのメソッドをstubする
      • 特定のオブジェクトのメソッドをstubでmockオブジェクトを返す
    • ::stub_any_instance
      • 特定のクラスのインスタンスメソッドをstubする
      • 特定のクラスのインスタンスメソッドをstubでmockオブジェクトを返す

前提として、minitestのstubはブロック内のみでstubが有効になる。rspecではit内すべてで有効になる。

本記事では下記Hogeクラスというコードをstubしていく。

class Hoge
  def run
    internal
  end

  def internal
    'hello'
  end
end

特定のオブジェクトのメソッドをstubする

まず特定のオブジェクトに対してstubするには、

hoge = Hoge.new
hoge.stub :internal, 'hello from mock' do 
   hoge.run # => 'hello from mock'
end
hoge.run # => 'hello'

というように書ける。

特定のオブジェクトのメソッドをstubでmockオブジェクトを返す

次に前述のstubにmockを返して、メソッドが呼ばれたか確認する。

hoge = Hoge.new
mock = Minitest::Mock.new.expect(:call, true, [])
hoge.stub :internal, mock do 
  hoge.run # => 'hello from mock'
end
mock.verify # => true

というように書ける。
Minitest::Mock#verifyはexpect で設定したメソッドが呼ばれない状態で実行すると例外になる。

特定のクラスのインスタンスメソッドをstubする

次は、特定のクラスすべてのインスタンスメソッドをstubする。
rspec3系だと下記のように書くコードをminitestで書く。

allow_any_instance_of(Object).to receive_messages(:foo => 'foo', :bar => 'bar')
o = Object.new
expect(o.foo).to eq('foo')
expect(o.bar).to eq('bar')

rpsecではallow_any_instance_ofの挙動をするメソッドはデフォルトで使えるけどminitestではminitest-stub_any_instanceというgemで使えるようになる。

Hoge.stub_any_instance :internal, 'hello from mock' do 
  Hoge.new.run # => 'hello from mock'
end
Hoge.new.run # => 'hello'

というように書ける。

特定のクラスのインスタンスメソッドをstubでmockオブジェクトを返す

次はstub_any_instanceでmockオブジェクトを返す。

mock = Minitest::Mock.new.expect(:call, true, [])
Hoge.stub_any_instance :internal, mock do 
  Hoge.new.run # => 'hello from mock'
end
mock.verify

で動くと思ってしまいがちだけど、実は下記のようなエラーになる。

TypeError: wrong argument type Minitest::Mock (expected Proc)
from /usr/local/lib/ruby/gems/2.4.0/gems/minitest-stub_any_instance-1.0.1/lib/minitest/stub_any_instance.rb:10:in `block (2 levels) in stub_any_instance'

この挙動は、mockオブジェクトを受け付けないことを示していて、下記プルリクによる影響ということがわかった。
https://github.com/codeodor/minitest-stub_any_instance/pull/2/files

要は今の仕様では.stub_any_instanceにmockオブジェクトは渡すことはできないため、このgemにプルリクを送るかアプリケーションの設計を見直すか下記のようなモンキーパッチを書くかなどをしなければならない

    class_eval do
      alias_method new_name, name

      define_method(name) do |*args|
        if val_or_callable.respond_to? :call then
          if val_or_callable.class == Proc
            instance_exec(*args, &val_or_callable)
          else
            val_or_callable.call(*args)
          end
        else
          val_or_callable
        end
      end
    end

まとめ

  • minitestのstubはブロックの中だけ有効
  • .stub_any_instanceの引数にmockを直接渡せない

完。