モジュールの特異メソッドを上書きする、しかもローカル変数を使って。

Qiitaにも投稿してます→ http://qiita.com/k5trismegistus/items/872d94c19dfd5d54d263 

 

TL;DR

モジュールFooに定義した特異メソッドbarを上書きしたい場合は、

Foo.module_exec do
define_singleton_method(:hoge) do
'overriden'
end
end
 
とすればよい。
また、

fuga = 'hoge'

Foo.module_exec(fuga) do |arg|
define_singleton_method(:hoge) do
arg
end
end
 
とすれば、モジュール外のローカル変数をメソッド定義の中で使うこともできる。(こういうのがクロージャというんでしょうか?)
 

 



モジュールの特異メソッドを上書きする、しかもローカル変数を使って。

モジュールの特異メソッドをスタブしたかった

includeしたりextendしたりするためでなく、単にユーティリティ関数みたいなメソッドを集めたモジュールを作ることがあると思います。Mathモジュールのようなイメージです。
そういったモジュールは

module Foo
def self.bar
'bar' * rand(1..10)
end
end
 
といったようにモジュールに特異メソッドをもたせます。
このメソッドはランダム要素があるため、呼び出されるたびに結果が変わりえます。
RSpecでユニットテストを書く際、このメソッドを決まった返り値を返すようにスタブしたいと思った場合はどうするでしょうか?
単純に

before do
allow(Foo).to receive(:bar).and_return('barbarbar')
end
 
としてやればOKです。

引数を取る場合

では、メソッドが引数を取る場合はどうでしょうか。

module Fooo
def self.baar(arg)
arg * rand(1..10)
end
end
 
メソッドがいくつか異なる引数で呼ばれ、それぞれ違う結果を返すようにスタブしたい場合はどうしたらいいでしょうか?(スタブという言葉の使い方が間違っているかもしれません><)

let(:arg1) { 'hoge' }
let(:expected_return1) { 'hogehoge' }
# Fooo.baar('hoge') は必ず 'hogehoge' を返してほしい

let(:arg2) { 'fuga' }
let(:expected_return2) { 'fugafugafugafuga' }
# Fooo.baar('fuga') は必ず 'fugafugafugafuga' を返してほしい
 
まず、単純にこれを考えます。

before do
allow(Foo).to receive(:baar).with('hoge').and_return('hogehoge')
allow(Foo).to receive(:baar).with('fuga').and_return('fugafugafugafuga')
end
 
あとで試したところ、全然これでよかったのですが、、なぜかこれがうまく動かないときがありました。
typoしてただけなのかもしれませんが、てっきり複数のallowはダメだと思いこんでしまい…
そこで モンキーパッチしてモジュール自体を書き換えようと思いました。

module Fooo
def self.baar(arg)
if arg == 'hoge'
'hogehoge'
elsif arg == 'fuga'
'fugafugafugafuga'
else
raise
end
end
end
 

外部の変数を使ってメソッドを定義したい

しかし、これだとせっかく期待する引数と返り値をletで宣言的に定義した意味がありません。
かといって、当然

let(:arg1) { 'hoge' }
let(:expected_return1) { 'hogehoge' }

let(:arg2) { 'fuga' }
let(:expected_return2) { 'fugafugafugafuga' }

module Fooo
def self.baar(arg)
if arg == arg1
expected_return1
elsif arg == arg2
expected_return2
else
raise
end
end
end
 
としてもだめです。moduledefはスコープを作るので、外のローカル変数は参照できないからです。
Rubyには、スコープを作らずにクラスやモジュールの中に入れるxxxx_eval系メソッド、同じくスコープを作らずにメソッドを定義できるdefine_method系メソッドがあります。それらを使えば望みどおりのことができました。

let(:arg1) { 'hoge' }
let(:expected_return1) { 'hogehoge' }

let(:arg2) { 'fuga' }
let(:expected_return2) { 'fugafugafugafuga' }

Fooo.module_exec(arg1, arg2, expected_return1, expected_return2) do |a1, a2, r1, r2|
define_singleton_method(:baar) do |arg|
if arg == a1
r1
elsif arg == a2
r2
else
raise
end
end
end
 
モジュールに対してmodule_execを適用すると、引数に渡した変数への参照をもちつつモジュール定義の中にはいることができます。
module_execメソッドに渡すブロックの中に実行したい処理を記述します。(ブロック引数がさきほどmodule_execに渡した引数に対応)
また、define_singleton_methodメソッドは、引数に渡した文字列・シンボルを名前にした特異メソッドを定義します。メソッドの中身はブロックで渡します。
こちらは全くスコープを作らないので、そのままa1, a2といった変数が参照できています。