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.