Make Thread synchronous in Rspec
2021-09-25
Rubytl;dr
allow(Thread).to receive(:new).and_yield
What's the problem?
In Ruby, Thread can be used for asynchronous processings that would take a while.
However, the asynchronousity sometimes cause unexpected results in unit tests, like Rspec.
Say, a class that uses Thread internally to run something in background thread.
class MyThread
def initialize(&callback)
@callback = callback
end
def run
Thread.new { @callback.call }
end
end
MyThread.new { do_somethings() }.run # to run do_somethings() function asyncronously
How to test MyThread#run to make sure do_somethings function is executed as expected?
Kernel::sleep
Which is I think the easiest way to tackle with asynchronousity, but as you know it's too primitive and makes the test unstable.
A test with Kernel::sleep would look like the following snippet.
it 'can complete after sleep a while' do
my_thread = MyThread.new { STDOUT.puts("hello") }
expect(STDOUT).to receive(:puts).with("hello").once
my_thread.run
sleep 1 # required to wait for Thread.
end
Yes, it would work, but you have to wait a second for the test to be completed, which is too long.
sleep 0.1 also may work, but how to guarantee that? Nobody knows the best duration to sleep.
Stub Thread
Here is the solution I propose.
before do
allow(Thread).to receive(:new).and_yield
end
it 'runs asynchronously' do
my_thread = MyThread.new { STDOUT.puts("hello") }
expect(STDOUT).to receive(:puts).with("hello").once
my_thread.run
end
This test must suceeed.
and_yield to call a given proc immediately. In this case, { STDOUT.puts("hello") } is executed when Thread.new is called.
See the document for the details.
https://relishapp.com/rspec/rspec-mocks/docs/configuring-responses/yielding
Note that if you call Thread#join after calling Thread.new, it should raise an error because of the stub.
Thread.new { do_somethings() } with allow(Thread).to receive(:new).and_yield returns not Thread but the result of do_somethings(), which obviously doesn't have #join method.
Hence, in such case, need to stub Thread#join or even wrap Thread#join with begin-rescue clause.