見出し画像

【改善版】.mock_in_sequence を作ってみました。

前回の投稿。

前回のresults.verify(self)のselfの不気味さをなんとかせねばとポンコツ君は奮闘しましたとさ。

気負い過ぎてしまった感がアリアリだったので、少し基本に立ち返ることにしました。次の仕様に変更することにします。

mock_in_sequence(object, target_method, exp_sequence_args)

レシーバを引数に収めて、関数的に仕上げた方が、よっぽどマシであるとの思いに至りました。mock_in_sequenceは、MockVerifyのファクトリーなんだと云う見方で納得することもできますけどね。

改善後のモジュールは、次の通り。

# 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, args)|
      mocks[name] = MiniTest::Mock.new
      if args.nil?
        mocks[name].expect(:call, retval) { act_sequence << name }
      else
        mocks[name].expect(:call, retval) { act_sequence << name; 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

今回、特に勉強になる着眼点はありません。mock_in_sequenceのレシーバの引数に移動したことによる変更だけです。これによって、results.verify(self)の不気味さが無くなり、results.verifyとするだけで良くなりました。
敢えて着眼点を申し上げるとするならば、コメントを大量に書き込んだことです。引数の数が多いのと使い方が特殊なので、忘れてしまった時のため、積極的に入れてみました。

サンプルのクラスは、前回と同じ。こちら。

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
    # @my_class.extend MockInSequence # 不要になったので、コメントアウト
    
    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
    # @my_class.extend MockInSequence # 不要になったので、コメントアウト
    
    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

モジュールは、インスタンスにエクステンドしていたのが、テストのクラスにインクルードする形に変わりました。

results = mock_in_sequence(@my_class, :show, exp_sequence_args) results.verify

気持ち悪さが無くなり、断然マシになりました。実際に使ってもらえると努力が報われると云うもの。是非是非、お試しあれ。


追記 (2022/6/4):お詫びと報告
あぁ。。。引数が付いた場合の呼び出しで、引数の評価をきちんとしていないです。重大な欠陥ですね。やっぱり、ポンコツでした。なんとかせねば。

追記 (2022/6/4):修正を施した【陳謝版】は、こちら。

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