問題
BasicObject#method_missing を使って slice_hoge
という名前のメソッドを動的に定義する。
class Mole < ApplicationRecord
# mole.slice_hoge で mole.slice(:hoge) を呼び出す。
# ただし hoge の部分は Mole のカラム名に限定する。
def method_missing(name, ...)
column_name = name.to_s.slice(/\Aslice_(\w+)\z/, 1)&.to_sym
return super unless column_name
column_names = self.class.column_names.map(&:to_sym)
return super unless column_name.in?(column_names)
slice(column_name)
end
end
mole = Mole.first!
mole.slice_name
#=> {"name"=>"クルテク"}
動的に定義したメソッドを RSpec で allow を使ってスタブしようとすると <#Mole ...> does not implement: slice_name
というエラーになる 😢
let(:mole) { FactoryBot.create(:mole) }
before do
allow(mole).to receive(:slice_name).and_return(name: 'もぐたろう')
end
# NG
# #<Mole id: 1, ...> does not implement: slice_name
it { expect(mole.slice_name).to eq(name: 'もぐたろう') }
解決方法
Object#respond_to_missing? をオーバーライドすれば OK
Ruby のリファレンスマニュアルにも
BasicObject#method_missing を override した場合にこのメソッドも override されるべきです。
と記載されている。そして RuboCop にも respond_to_missing?
の定義漏れを検知する Style/MissingRespondToMissing という Cop が存在する。
class Mole
def method_missing(name, ...)
return super unless respond_to?(name)
column_name = get_column_name_from_slice_method(name)
return super unless column_name
slice(column_name)
end
def respond_to_missing?(name, _include_private)
column_name = get_column_name_from_slice_method(name)
return super unless column_name
column_names = self.class.column_names.map(&:to_sym)
return super unless column_name.in?(column_names)
true
end
private
def get_column_name_from_slice_method(name)
name.to_s.slice(/\Aslice_(\w+)\z/, 1)&.to_sym
end
end
let(:mole) { FactoryBot.create(:mole) }
before do
allow(mole).to receive(:slice_name).and_return(name: 'もぐたろう')
end
# OK
it { expect(mole.slice_name).to eq(name: 'もぐたろう') }
バージョン情報
RUBY_VERSION
#=> "3.3.8"
RSpec::Mocks::Version::STRING
#=> "3.13.5"