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 育ちだと最初少しつまづくかもしれません。