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

(thanks to drbrain who pointed me to this option )

No matter which method you prefer it is not a bad idea to test things.