見出し画像

【陳謝版】.mock_in_sequenceを作ってみました。

適当な投稿をしてしまい申しわけありませんでした。

argsの付いた場合の評価がまったくされていなかったのですが、
なんとか修正が出来そうです。
まずは、修正結果から。

# Purpose: The methods in a target method are mocked in sequence
# mock_in_sequence(object, target_method, exp_sequence_args)
#   object:             testing object; instance object
#   target_method:      target method; symbol
#   exp_sequence_args:  exp_sequence_args is an array of methods
#                         and arguments that you want to mock in order
#   exp_sequence_args = [
#     [:method, retval, [args]],
#     [:method, retval, [args]],
#     ...
#   ]
# eg. class MyClass
#       def show
#         show_header
#         show_detail(arg1, arg2)
#         show_footer
#       end
#     end
#
#     class MyClassTest < MiniTest::Test
#       include MockInSequence
#
#       def setup
#         @my_class = MyClass.new
#       end
#
#       def test_show
#         exp_sequence_args = [
#           [:show_header, true],
#           [:show_detail, true, [arg1, arg2]],
#           [:show_footer, true]
#         ]
#         results = mock_in_sequence(@my_class, :show, exp_sequence_args)
#         results.verify
#       end
#     end
module MockInSequence
  def mock_in_sequence(object, target_method, exp_sequence_args)
    stub_stream   = "object.#{target_method};"
    act_sequence  = Array.new
    mocks         = Hash.new
    exp_sequence  = exp_sequence_args.map(&:first)
    exp_sequence_args.reverse!
    
    exp_sequence_args.each do |(name, retval, exp_args)|
      mocks[name] = MiniTest::Mock.new
      if exp_args.nil?
        mocks[name].expect(:call, retval) { act_sequence << name }
      else
        mocks[name].expect(:call, retval) { |*act_args| act_sequence << name; act_args == exp_args }
      end
      stub_stream = "object.stub(#{name.inspect}, mocks[#{name.inspect}]) do; #{stub_stream} end;"
    end
    eval(stub_stream)
    
    MockVerify.new({ mocks: mocks, expected: exp_sequence, actual: act_sequence }, self)
  end
  
  class MockVerify
    def initialize(results, test_obj)
      @test_obj = test_obj
      @mocks    = results[:mocks]
      @expected = results[:expected]
      @actual   = results[:actual]
    end
    
    def verify
      @mocks.values.map(&:verify)
      @test_obj.assert_equal @expected,
                             @actual,
                             'note: not expected the order of sequence.'
    end
  end
end

コード中、
 if exp_args.nil?
  mocks[name].expect(:call, retval) { act_sequence << name }
 else
  mocks[name].expect(:call, retval) { |*act_args| act_sequence << name;
   act_args == exp_args }

 end
と変更しています。
理由は、mock.rbの中を覗いてみると、

if val_block then
 # keep "verify" happy
 @actual_calls[sym] << expected_call

 raise MockExpectationError, "mocked method %p failed block w/ %p" %
 [sym, args] unless val_block.call(*args, &block)

 return retval
end

のコメントに# keep "verify" happyとある箇所です。
ここが、expectのブロック指定の処理になるようです。

val_block.call(*args, &block)を実行して、falseを返したらMockExpectationErrorになり、trueを返すとretvalを返して抜けるようになっています。

よく見ると、val_block.callの引数に*argsがあり、この中には、act側の
argsが入っています。
このact側のargsとexp側のargsをブロック内で比較してtrueを返せれば、
正常に抜けることができることになります。

ただ、残念なのは、テストに失敗するとMockExpectationErrorとしてerrorカウントされます。本来は、failureとしてカウントして欲しいのですが。。。

迷惑の掛かった方がみえましたら、申しわけなかったです。
もうちょっと気を付けないといけないですよね。反省。。。

全体のコードは、以下のようになります。

# Purpose: The methods in a target method are mocked in sequence
# mock_in_sequence(object, target_method, exp_sequence_args)
#   object:             testing object; instance object
#   target_method:      target method; symbol
#   exp_sequence_args:  exp_sequence_args is an array of methods
#                         and arguments that you want to mock in order
#   exp_sequence_args = [
#     [:method, retval, [args]],
#     [:method, retval, [args]],
#     ...
#   ]
# eg. class MyClass
#       def show
#         show_header
#         show_detail(arg1, arg2)
#         show_footer
#       end
#     end
#
#     class MyClassTest < MiniTest::Test
#       include MockInSequence
#
#       def setup
#         @my_class = MyClass.new
#       end
#
#       def test_show
#         exp_sequence_args = [
#           [:show_header, true],
#           [:show_detail, true, [arg1, arg2]],
#           [:show_footer, true]
#         ]
#         results = mock_in_sequence(@my_class, :show, exp_sequence_args)
#         results.verify
#       end
#     end
module MockInSequence
  def mock_in_sequence(object, target_method, exp_sequence_args)
    stub_stream   = "object.#{target_method};"
    act_sequence  = Array.new
    mocks         = Hash.new
    exp_sequence  = exp_sequence_args.map(&:first)
    exp_sequence_args.reverse!
    
    exp_sequence_args.each do |(name, retval, exp_args)|
      mocks[name] = MiniTest::Mock.new
      if exp_args.nil?
        mocks[name].expect(:call, retval) { act_sequence << name }
      else
        mocks[name].expect(:call, retval) { |*act_args| act_sequence << name; act_args == exp_args }
      end
      stub_stream = "object.stub(#{name.inspect}, mocks[#{name.inspect}]) do; #{stub_stream} end;"
    end
    eval(stub_stream)
    
    MockVerify.new({ mocks: mocks, expected: exp_sequence, actual: act_sequence }, self)
  end
  
  class MockVerify
    def initialize(results, test_obj)
      @test_obj = test_obj
      @mocks    = results[:mocks]
      @expected = results[:expected]
      @actual   = results[:actual]
    end
    
    def verify
      @mocks.values.map(&:verify)
      @test_obj.assert_equal @expected,
                             @actual,
                             'note: not expected the order of sequence.'
    end
  end
end

class MyClass
  def show
    show_header
    show_details
    show_footer
  end
  
  def show_header; end
  def show_details(condition = true); end
  def show_footer; end
end

class MySubClass < MyClass
  def show
    show_header
    show_footer
    condition = false
    show_details(condition)
  end
end

require 'minitest/autorun'
class MyClassTest < Minitest::Test
  include MockInSequence

  def setup
    @my_class = MyClass.new
  end

  def test_show
    exp_sequence_args = [
      [:show_header,  true],
      [:show_details, true],
      [:show_footer,  true]
    ]
    results = mock_in_sequence(@my_class, :show, exp_sequence_args)
    results.verify
  end
end

class MySubClassTest < Minitest::Test
  include MockInSequence

  def setup
    @my_sub_class = MySubClass.new
  end
  
  def test_show
    condition = false
    exp_sequence_args = [
      [:show_header,  true],
      [:show_footer,  true],
      [:show_details, true, [condition]]
    ]
    results = mock_in_sequence(@my_sub_class, :show, exp_sequence_args)
    results.verify
  end
end

この記事が気に入ったらサポートをしてみませんか?