LoginSignup
0
0

More than 5 years have passed since last update.

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

Posted at

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モジュールのようなイメージです。
そういったモジュールは

foo.rb
module Foo
  def self.bar
    'bar' * rand(1..10) 
  end
end 

といったようにモジュールに特異メソッドをもたせます。

このメソッドはランダム要素があるため、呼び出されるたびに結果が変わりえます。
RSpecでユニットテストを書く際、このメソッドを決まった返り値を返すようにスタブしたいと思った場合はどうするでしょうか?
単純に

foo_spec.rb
before do
  allow(Foo).to receive(:bar).and_return('barbarbar')
end

としてやればOKです。

引数を取る場合

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

fooo.rb
module Fooo
  def self.baar(arg)
    arg * rand(1..10)
  end
end

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

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

let(:arg2) { 'fuga' }
let(:expected_return2) { 'fugafugafugafuga' }
# Fooo.baar('fuga') は必ず 'fugafugafugafuga' を返してほしい

まず、単純にこれを考えます。

fooo_spec.rb
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はダメだと思いこんでしまい…
そこでモンキーパッチしてモジュール自体を書き換えようと思いました。

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

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

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

fooo_spec.rb
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系メソッドがあります。それらを使えば望みどおりのことができました。

fooo_spec.rb
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といった変数が参照できています。

ちなみに、自分のブログにものせてます→リンク

0
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0