RSpec の stub! とプライベートメソッドの話

プライベートなメソッドを stub! でスタブすると次の example 内でメソッドが消えている、という話を聞きました。
privateのクラスメソッドをスタブにすると、そのメソッドをSpec上で呼び出すときに、undefined method になる - かせいさんとこ

example が評価された後、終了処理が行われます。example オブジェクトの after_each_example がこれに該当します。そして、その中で teardown_mocks_for_rspec が実行されます。

teardown_mocks_for_rspec はモックフレームワークで定義されるメソッドで、標準のモックフレームワーク rspec の中では、$rspec_mocks.reset_all が実行される様になっています。

$rspec_mocks.reset_all は、作られた全てのモックオブジェクトに対して rspec_reset を実行します。

rspec_reset はプロキシオブジェクト(Spec::Mocks::Proxy)の reset を実行します。

reset メソッドの中では、clear_expectations, clear_stubs, reset_proxied_methods, clear_proxied_methods, reset_nil_expectations_warning といったメソッドが順に実行されます。

clear_* メソッドは、Array#clear なので特別な事はおきません。今回の問題に関わるのは reset_proxied_methods メソッドです。

reset_proxied_methods は、名前の通りプロキシされたメソッド達それぞれを、リセットするというものです。実際のリセットは reset_proxied_method メソッドで行います。

reset_proxied_method(method_name) によって、method_name という名前のメソッドと munge(method_name) で得られる名前のメソッドが remove_method によって削除されます。

munge(method_name) で得られるメソッドについては、alias_method(method_name, munge(method_name)) で退避してから削除しています。なぜこんな事をするかと言えば、元々存在していたメソッドをスタブで置き換える場合、munge(method_name) という名前で退避していたからです。

さて、プライベートメソッドの場合に、reset 後にメソッドが削除されてしまうのはなぜでしょう。元に戻してくれるはずではないのでしょうか。

問題は、スタブを作る(定義する) Spec::Mocks::Proxy#define_expected_method の中にあります。メソッドが「応答可能」な場合にのみ、alias_method(munge(original_name), original_name) によってオリジナルを退避します。

ここでメソッドの応答/実在の判定に使われているのは、respond_to? と method_defined? です。これらは、いずれもプライベートメソッドについては偽を返す様です。

>> c = Class.new do
     def pub; 1; end
     private
     def pri; 2; end
   end
=> #<Class:0x10194b1a8>
>> obj = c.new
=> #<#<Class:0x10194b1a8>:0x101941db0>
>> obj.respond_to?(:pub)
=> true
>> obj.respond_to?(:pri)
=> false
>> obj.respond_to?(:pri, true)
=> true
>> c.instance_eval do
     p method_defined?(:pub)
     p method_defined?(:pri)
     p private_method_defined?(:pri)
   end
true
false
true
=> nil

一方で、メソッドの置き換えを行う際は、visibility メソッドで public/private/protected を判断し、適した宣言を行っています。

def define_expected_method(sym)
  unless @proxied_methods.include?(sym)
    visibility_string = "#{visibility(sym)} :#{sym}"

    if target_responds_to?(sym)
      munged_sym = munge(sym)
      target_metaclass.instance_eval do
        alias_method munged_sym, sym if method_defined?(sym)

      end
      @proxied_methods << sym
    end
    target_metaclass.class_eval(<<-EOF, __FILE__, __LINE__)

      def #{sym}(*args, &block)
        __mock_proxy.message_received :#{sym}, *args, &block
      end

      #{visibility_string}
    EOF
  end
end

そんな訳で、respond_to?(method_name) が真を返さないメソッドをスタブすると、次の example の時には消されてしまう様です。

さて、これは意図通りなのか、バグなのか、モックやスタブを理解していない身としては、判断がつきません…。

プロフィール

このブログ記事について

このページは、koshigoeが2009年12月 6日 00:30に書いたブログ記事です。

ひとつ前のブログ記事は「Apache の mod_proxy_balancer のスケジューリングアルゴリズム」です。

次のブログ記事は「MT5 にアップグレードしてみた」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。