I'm writing some code using this gem called kubernetes_leader_election
which helpfully has a full example showing how to use it in its README. My usage of it is basically the same as what the README says, but here are the most relevant lines:
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
I'm trying to write a spec that tests that the lease_name
I passed in is what I expect, and that the code will not sleep forever if the block, { is_leader = true }
, executes. I know how to mock KubernetesLeaderElection.new
, but I'm kind of new to using rspec mocks, so I'm really struggling to mock elector.become_leader_for_life
with the same robustness (being able to test what it gets passed.) I've tried a bunch of variations on this overall concept, but none do quite what I need, and most give errors. Here's my latest attempt, which doesn't fail, but doesn't test everything I want:
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
(EDIT with update: If I use .and_yield
, I can get past the problem of rspec hanging, but it's not a full solution yet, because I can't test an actual match of the block contents because .and_yield
makes my block not be the end of the chain, more info here. If I move .and_yield
before the block, then expect(block)
is expect(nil)
. No idea why.)
The problems I've had with my approaches have been either that rspec hangs because is_leader = true
is not executed, so sleep 1 until is_leader
will run forever, or (with my latest implementation) I can get the original block to execute, but I can't make assertions that it did, I can only infer that it did because the spec ends. I have tried various suggestions from StackOverflow and GitHub threads that say I should be able to say expect(block).to be
either the string that is the exact contents of the block code, or a Proc that contains the same code, but approaches don't work because the actual value is a Proc (so the suggestions to use strings don't work) and it's not the same Proc object (so be
and eq
don't work.) I have also tried attaching .and_wrap_original
to various places, but that either gives me an error or gives me the same behavior as this example. The best I can do is match
to at least check that the return value of the block is the same as the Proc I make. I am utterly lost. I know I can just switch to using an instance variable, but I don't want to expose that to classes including my mixin.
So to summarize, I:
is_leader = true
, to actually execute, and would like to test that its contents are what I specify;elector.become_leader_for_life
, to not call the original code (because it's a third party library and I don't want to mock details of its implementation;)Thread.new
, actually executes (the spec doesn't need the code to be multithreaded, that's only there for runtime efficiency, hence why I mocked out sleep
for my specs.)How can I achieve this "mocking only the middle block" behavior without resorting to tainting the code with mocking smells like using instance variables?
I'm not familiar with what this does:
expect(block).to be("is_leader = true")
but the code:
expect(dummy_elector).to receive(:become_leader_for_life) do |&block|
expect(block).to be("is_leader = true")
end
doesn't ever execute the block it's receiving, so is_leader
will never be changed.
Assuming you can get the source of a block, could you do something like:
expect(dummy_elector).to receive(:become_leader_for_life) do |&block|
block.call
end
potentially with the block.call
conditional on the block source?