Testing Infinite Loop in Ruby

It happens very rarely but sometimes you have to deal with infinite loops.

Let us say there is a worker, that runs as a separate process in a loop.
It may constantly check database and do something useful.

class Worker
def self.start
loop do
# do useful work
end
end
end

The question is: how do we unit test such worker? If we execute
Worker.start within a test, the control flow will be given to the endless loop and
the test will stuck forever.

Step 1: Extract the loop block into a testable unit

We should keep the loop block as small as possible and move the logic into a separated unit, that can be easily tested.
Let us say we extract the logic within loop block into DoUsefulWork service object.

Then we get a code similar to the following:

class DoUsefulWork
def self.call
# do useful work
end
end
class Worker
def self.start
loop do
DoUsefulWork.call
end
end
end

Now DoUsefulWork service object can be tested as anything else in your code base.

But Worker.start still remains untested... You may be OK with this.
But if you're a paranoiac like me and targeting 100% test coverage or just want to ensure that
you do not mistype DoUesfulWork please read further.

Step 2 (a): Make use of timeouts

We probably want to ensure, that Worker.start do not raise any ridiculous exceptions like
NameError (uninitialized constant DoUesfulWork) but at the same time we do not want to let it run forever.

That is a case where we can utilize
Timeout.timeout
method from the ruby standard library. From the documentation:

Perform an operation in a block, raising an error if it takes longer than sec seconds to complete.

So we can wrap invocation of Worker.start into Timeout.timeout block.
The chosen timeout must be very small but reasonable for your particular case to ensure, that
the iteration is executed at least once.

Assuming we are using RSpec for testing, the test may look like
the following:

RSpec.describe Worker do
it 'does not raise' do
Timeout.timeout(0.001) do
expect { described_class.start }.not_to raise_error
end
end
end

The test does not hang forever, but it fails with Timeout::Error.
So now we need to suppress this error, wrapping the execution with begin/rescue/end block
(alternatively we can use
Kernel#suppress
method from ActiveSupport which does exactly the same):

RSpec.describe Worker do
it 'does not raise' do
begin
Timeout.timeout(0.001) do
expect { described_class.start }.not_to raise_error
end
rescue Timeout::Error
end
end
end

Such test would pass but it looks a little bit noisy and smells.
Let us extract the timeout wrapper into within_timeout test helper:

RSpec.describe Worker do
def within_timeout(seconds)
Timeout.timeout(seconds) do
yield
end
rescue Timeout::Error
end
it 'does not raise' do
within_timeout(0.001) do
expect { described_class.start }.not_to raise_error
end
end
end

Step 2 (b): Stub loop method

If you do not feel comfortable using Timeout, there is an alternative way.
loop is a regular method in Ruby, that comes from Kernel.
That means it can be stubbed as any other method.

RSpec.describe Worker do
it 'does not raise' do
allow(described_class).to receive(:loop) do |&block|
expect { block.call }.not_to raise_error
end
described_class.start
expect(described_class).to have_received(:loop)
end
end