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.