Techioz Blog

rspec を使用してネストされたブロックの中間層のみをモックする

概要

私は kubernetes_leader_election と呼ばれるこの gem を使用してコードを書いています。この gem の README には、使い方を示す完全な例が記載されています。私の使い方は基本的に README に記載されている内容と同じですが、最も関連性の高い行は次のとおりです。

lease_name = self.class.name.underscore.gsub(/[\/_]+/, "=")
is_leader = false
elector = KubernetesLeaderElection.new(lease_name, kubeclient, logger: Rails.logger)
Thread.new { elector.become_leader_for_life { is_leader = true } }
sleep 1 until is_leader

渡したリース名が期待どおりであること、およびブロック { is_leader = true } が実行された場合にコードが永久にスリープしないことをテストする仕様を書こうとしています。 KubernetesLeaderElection.new をモックする方法は知っていますが、rspec モックの使用には慣れていないため、elector.become_leader_for_life を同じ堅牢性 (何が渡されるかをテストできる) でモックするのに非常に苦労しています。この全体的な概念についてさまざまなバリエーションを試しましたが、どれも私が必要とするものを完全に満たすものはなく、ほとんどがエラーを引き起こします。これが私の最新の試みです。これは失敗しませんでしたが、必要なものすべてをテストしたわけではありません。

describe LeaderElectionMixin
  class MixinUser
    include LeaderElectionMixin
  end

  describe "#wait_to_be_leader" do
    let(:dummy_client) { double(Kubeclient::Client) }
    let(:dummy_elector) { double(KubernetesLeaderElection) }

    before do
      allow(Kubeclient::Client).to receive(:new).and_return(dummy_client)
      expect(dummy_elector).to receive(:become_leader_for_life) do |&block|
        # only checks that the actual block and this proc return the same thing
        expect(block).to match(Proc.new { true })
      end.and_yield
    end

    it "should create leases based on the class name" do
      expect(KubernetesLeaderElection).to receive(:new)
        .with("mixin-user", dummy_client, logger: Rails.logger)
        .and_return(dummy_elector)
      MixinUser.new.wait_to_be_leader
    end
  end
end

(更新を伴う編集: .and_yield を使用すると、rspec のハングの問題を回避できますが、まだ完全な解決策ではありません。.and_yield によってブロックが無効になるため、ブロックの内容の実際の一致をテストできないためです。チェーンの終わり、詳細については、ここを参照してください。ブロックの前に .and_yield を移動すると、expect(block) はexpect(nil) になります。)

私のアプローチで私が抱えていた問題は、is_leader = true が実行されないために rspec がハングすること、つまり is_leader が永遠に実行されるまでスリープ 1 すること、または (最新の実装では) 元のブロックを実行できるが、そうなったと断言することはできません。仕様が終了したためそうなったと推測することしかできません。 StackOverflow と GitHub のスレッドからのさまざまな提案を試しましたが、expect(block).to をブロック コードの正確な内容である文字列、または同じコードを含む Proc のいずれかにすることができるはずですが、アプローチは適切ではありません。実際の値は Proc であり(文字列を使用するという提案は機能しません)、同じ Proc オブジェクトではないため(つまり be と eq は機能しません)、機能しません。また、.and_wrap_original をさまざまな場所にアタッチしようとしましたですが、エラーが発生するか、この例と同じ動作が発生します。私にできる最善のことは、少なくともブロックの戻り値が私が作成した Proc と同じであることを確認するために照合することです。まったく道に迷ってしまいました。インスタンス変数の使用に切り替えることができることはわかっていますが、それをミックスインを含むクラスに公開したくありません。

要約すると、私は次のように言います。

インスタンス変数を使用するようなモック臭でコードを汚染することなく、この「中央のブロックのみをモックする」動作を実現するにはどうすればよいでしょうか?

解決策

これが何をするのかよくわかりません:

expect(block).to be("is_leader = true")

しかし、コード:

expect(dummy_elector).to receive(:become_leader_for_life) do |&block|
  expect(block).to be("is_leader = true")
end

は受信したブロックを実行しないので、 is_leader が変更されることはありません。

ブロックのソースを取得できると仮定すると、次のようなことができますか?

expect(dummy_elector).to receive(:become_leader_for_life) do |&block|
  block.call
end

ブロックソースの block.call 条件を使用する可能性はありますか?