In this code, sync_external
accepts an array of posts and modifies it by calling a sync method from an external module ExternalService
, which is mocked in the test.
Assume there's some reason to pass a block to sync
. In this trivial example, the block exists to push every yielded response onto a responses array.
Code:
class Post
def self.sync_external(posts)
responses = []
ExternalService.sync(posts) do |response|
responses << response
end
return responses
end
end
RSpec:
let(:responses) {[]}
before do
allow(ExternalService).to receive(sync).with(anything)
.then{responses.inject(_1){|m, response| m.and_yield(response)}}
end
it "sync posts with 100 words successfully" do
posts = FactoryBot.build_list(:post, 10, words: 100)}
posts.each{|post| responses << 200}
expect(Posts.sync_external(posts)).to eq(Array.new(10, 200))
end
Actual result:
allow
evaluates in the before
block when the responses array was empty, so it yielded nothing.
Expected result:
allow
evaluates in the before
block, but re-evaluates and_yield
when the mocked method is called. This will let each it
assign different responses, while keeping the allow
mock in the before
block.
Edit (Solution):
Passing in a block argument and calling the block multiple times is equivalent to chaining .and_yield
multiple times, but useful for evaluating other RSpec variables at runtime.
allow(ExternalService).to receive(sync).with(anything) do |_, &b|
responses.each{b.call(_1)}
end
Thanks to @engineersmnky for his solution here: