読者です 読者をやめる 読者になる 読者になる

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

Programming/Ruby
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といった変数が参照できています。