見出し画像

インスタンス・オブジェクト内の異なるメソッドを順番にモックする方法を考えてみた。

Rubyで遊び始めて3年ほど経つでしょうか。それっぽい物も作れるようになってきたので、関心を持ってもらえそうなものを少しづつ投稿してみようと思います。
下のような2つのクラスにある.showメソッド内の異なるメソッドを実行順も併せてモックする必要が出てきたので、考えてみました。

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

ものすごくベタに書くとMiniTestでは、こうなるようです。

require 'minitest/autorun'
class MyClassTest < Minitest::Test
  def setup
    @my_class = MyClass.new
  end

  def test_show
    exp_sequence  = [:show_header, :show_details, :show_footer]
    pass_sequence = []

    mock_show_header  = MiniTest::Mock.new
    mock_show_details = MiniTest::Mock.new
    mock_show_footer  = MiniTest::Mock.new

    # {...}は、mockからは無視されている
    mock_show_header.expect(:call, true) { pass_sequence << :show_header }
    mock_show_details.expect(:call, true) { pass_sequence << :show_details }
    mock_show_footer.expect(:call, true) { pass_sequence << :show_footer }

    @my_class.stub :show_header, mock_show_header do
      @my_class.stub :show_details, mock_show_details do
        @my_class.stub :show_footer, mock_show_footer do
          @my_class.show
        end
      end
    end

    mock_show_header.verify
    mock_show_details.verify
    mock_show_footer.verify
    assert_equal exp_sequence,
                 pass_sequence,
                 'note: not expected the order of sequence'
  end
end

class MySubClassTest < Minitest::Test
  def setup
    @my_sub_class = MySubClass.new
  end

  def test_show
    condition     = false
    exp_sequence  = [:show_header, :show_footer, :show_details]
    pass_sequence = []

    mock_show_header  = MiniTest::Mock.new
    mock_show_details = MiniTest::Mock.new
    mock_show_footer  = MiniTest::Mock.new

    mock_show_header.expect(:call, true) { pass_sequence << :show_header }
    mock_show_details.expect(:call, true) { pass_sequence << :show_details; [condition] }
    # 最後に実行した[condition]が、mock_show_detailsの引数として評価される
    mock_show_footer.expect(:call, true) { pass_sequence << :show_footer }

    @my_sub_class.stub :show_header, mock_show_header do
      @my_sub_class.stub :show_details, mock_show_details do
        @my_sub_class.stub :show_footer, mock_show_footer do
          @my_sub_class.show
        end
      end
    end

    mock_show_header.verify
    mock_show_details.verify
    mock_show_footer.verify
    assert_equal exp_sequence,
                 pass_sequence,
                 'note: not expected the order of sequence'
  end
end

.expectは、モックするメソッドが

  • 引数を伴わない時、ブロック内の評価は無視

  • 引数を伴う時は、モックの引数とブロック内の最終評価を比較する

ようです。(2022/6/4 完全に間違えています。修正を検討しなければいけなくなりました。)

.expectのブロック内へ、pass_sequenceに通過したメソッドのシンボルを溜め込むことで、実行順を捕捉しています。他の部分は、通常の使い方のままですので、説明は不要だと思います。

ものすごくベタな解決方法なので、
メソッドにまとめた【ポンコツ版】.mock_in_sequenceを考えてみたのですが、あまりにポンコツで恥ずかしいですが、後日、投稿したいと思います。

なにぶん、本人もポンコツなので、アドバイスなどが貰えたら幸いです。

追記:なぜか続きの記事が表示されていないようなので、ここにリンクを入れておきます。


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