Techioz Blog

Ruby on Rails Minitest 1 つのテストで複数のモックをテストする

概要

Ruby on Rails で Minitest を使用して単体テストを作成しています。

時々、一度に複数のものをモックする必要があります。

たとえば、ユーザーへの通知をトリガーするアクションをテストする場合、2 つの外部サーバー (例: SmsClient と EmailClient) を一度にモックしたい場合があります。

これは次のようにして実行できます。

test 'my test case' do
  SmsClient.stub :send, nil do
    EmailClient.stub :send, nil do
      get my_controller_url
    end
  end
end

これは機能しますが、同じ呼び出しでもう 1 つのクラスをモックする必要があります。

これらのスタブ呼び出しのネストが制御不能になるのではないかと心配しています。

質問: ネストを使用せずにこれらのサービスにモックをセットアップする方法はありますか?

注: これらの例は単なる仮説です。

解決策

これを簡素化する Minitest ヘルパーがあるかどうかはわかりませんが、ネストを減らすことができるコアの Ruby コードをいくつか示します。

stubs = [
  proc { |block| proc { SmsClient.stub(:send, nil, &block) } },
  proc { |block| proc { EmailClient.stub(:send, nil, &block) } },
]

tests = proc do
  # ...
end

stubs.reduce(:<<).call(tests).call
# results in
# (stubs[0] << stubs[1]).call(tests).call
#
# aka
# stubs[0].call(stubs[1].call(tests)).call
#
# aka
# SmsClient.stub(:send, nil) do
#   EmailClient.stub(:send, nil) do
#     # ...
#   end
# end

この答えを理解するには、次のことを知っておく必要があります。

some_method :a, :b do
  # block code
end

次のように書くこともできます。

block = proc do
  # block code
end

some_method(:a, :b, &block)

subs 配列内の各項目は proc であり、ブロックを受け取り、別の要素に渡される新しい proc を返します。

stubs.reduce(:<<) は、任意の引数を最後の要素に渡す合成プロシージャを作成します。そのプロシージャの戻り値は、その前の要素に渡されます。

double = proc { |n| n + n }
pow2   = proc { |n| n * n }

operations = [pow2, double]
operations.reduce(:<<).call(2) # the pow2 of the double of 2
# pow2.call(double.call(2))
# a = 2 + 2 = 4
# b = a * a = 4 * 4 = 16
#=> b = 16
stubs.reduce(:<<).call(tests) # does the same
# stubs[0].call(stubs[1].call(tests))
# a = proc { EmailClient.stub(:send, nil, &tests) }
# b = proc { SmsClient.stub(:send, nil, &a) }
#=> b = proc { SmsClient.stub(:send, nil, &proc { EmailClient.stub(:send, nil, &tests) }) }

b は依然として proc であるため、最終的な戻り値で .call を再度呼び出す必要があることに注意してください。

上記のコードで何が起こっているかを理解するのに役立つ画像を次に示します。

:<< を :>> に変更すると、ネストを反転できることに注意してください。

ヘルパーを定義して共通ロジックを抽象化できます。

def stub_all(*stubs, &test_block)
  stubs.map! do |subject, *stub_args|
    proc { |block| proc { subject.stub(*stub_args, &block) } }
  end

  stubs.reduce(:<<).call(test_block).call
end
stub_all(
  [SmsClient, :send, nil],
  [EmailClient, :send, nil],
) do
  # ...
end