Testing Infinite Loop in Ruby
Serhii Potapov February 13, 2019 #rubyIt 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.
    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:
    # do useful work
  end
end
    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  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  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
  
    Timeout.timeout(seconds) do
      yield
    end
  rescue Timeout::Error
  end
  it  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  do
    allow(described_class).to receive(:loop) do 
      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.