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.