RubyのMochaはメタボ気味だった件について

id:suerさんとRubyのスタブ/モックフレームワークMochaを追いかけてみたら、わりとメタボ(メタプログラミング)気味だった件について。

まとめ

Mochaはテストフレームワークのメソッドを書き換えて、魔術的なことを実現してるよ!

挙動

Mochaを使うとこんなことができるよ!

def test_f
  o = SomeClass.new
  # bar(4)が呼ばれることを期待するモック
  o.expects(:bar).with(4)
  o.foo(2)

  # o#bar(4)が呼ばれてないと、ここでエラーが発生する(!)
end

なんだかメタボっぽいので、おっかけてみました。

expectsの登録

expectsが呼ばれると、その情報がexpectationsに登録されます。

# lib/mocha/mock.rb
def expects(method_name_or_hash, backtrace = nil)
  iterator = ArgumentIterator.new(method_name_or_hash)
   iterator.each { |*args|
     method_name = args.shift
     ensure_method_not_already_defined(method_name)
     expectation = Expectation.new(self, method_name, backtrace)
     expectation.returns(args.shift) if args.length > 0
     @expectations.add(expectation)
  }
end

呼ばれたかどうかのチェック

Mockery#verifyが呼ばれると、全部のexpectationsが呼ばれたかどうかをチェックします。

# lib/mocha/mockery.rb
def verify(assertion_counter = nil)
  unless mocks.all? { |mock| mock.__verified__?(assertion_counter) }
     message = "not all expectations were satisfied\n#{mocha_inspect}"
     if unsatisfied_expectations.empty?
       backtrace = caller
     else
       backtrace = unsatisfied_expectations[0].backtrace
    end
    raise ExpectationError.new(message, backtrace)
  end
  expectations.each do |e|
    unless Mocha::Configuration.allow?(:stubbing_method_unnecessarily)
      unless e.used?
        on_stubbing_method_unnecessarily(e)
      end
    end
  end
end

テスト終了後にverifyの呼び出し

ここからがいよいよメタボ(メタプログラミング)っぽいところです。

まずmonkey_patchメソッドで、フレームワークにあったパッチをrequireしてます。

# lib/mocha/integration.rb
def monkey_patches
  patches = []
  if test_unit_testcase_defined? && !test_unit_testcase_inherits_from_miniunit_testcase?
     patches << 'mocha/integration/test_unit'
  end
  if mini_unit_testcase_defined?
    patches << 'mocha/integration/mini_test'
  end
  patches
end

# (略)

Mocha::Integration.monkey_patches.each do |patch|
  require patch
end

そして、requireするパッチは、各バージョンごと(!)に用意されています。

$ ls integration/test_unit
assertion_counter.rb           gem_version_203_to_209.rb
gem_version_200.rb             ruby_version_185_and_below.rb
gem_version_201_to_202.rb      ruby_version_186_and_above.rb

$ ls integration/mini_test
assertion_counter.rb      version_140.rb
exception_translation.rb  version_141.rb
version_13.rb             version_142_and_above.rb

このパッチは、内部関数を書き換えることで、テスト終了後にverifyを呼び出すようにするものです。

# lib/mocha/integration/test_unit/gem_version_200.rb
def run(result)
  assertion_counter = AssertionCounter.new(result)
  begin
    @_result = result
    yield(Test::Unit::TestCase::STARTED, name)
    begin
      begin
        run_setup
        __send__(@method_name)
        mocha_verify(assertion_counter)
      rescue Mocha::ExpectationError => e
        ...
      end
    end
  end
end

結論

Mochaはメタボ気味。

追記

一緒にコードを読んでたsuerさんが、もっと詳細な記事(id:suer:20101005:1286293319)を書いてます。