POST DIRECTORY
Category software development

I’ve been working on a Rails 5 project that employs an infinite loop in conjunction with a few nested breakable loops. Code/case coverage is particularly important, as the program interacts with Cryptocurrency exchanges. Errors or unexpected behaviors can potentially result in financial loss.

I was expecting the loop tests to be convoluted, heavy on setup, and generally difficult to present in an easily discernible way. However I came across a couple approaches and helpers that made it surprisingly easy.

Take the following simplified case of a breakable loop:

class Decider

  def self.undecided
    loop do
      break if decision?
    end
  end

  def self.decision?
    # code yielding true or false
  end
end

First I’d like to test that Decider.undecided breaks out of its loop when ‘a decision has been made’. This is pretty straightforward:

describe '.undecided' do
  context 'a decision has been made' do
    before do
      allow(Decider).to receive(:decision?) { true }
    end

    it 'breaks the loop' do
      expect(Decider).to receive(:decision?).once
      Decider.undecided
    end
  end
end

The trickier part is testing the alternative case in a way that doesn’t result in an infinite loop when running specs. For example…

allow(Decider).to receive(:decision?) { false }

…would lead to an infinite loop when the spec is run.

With RSpec Mocks we have at our disposal the more explicit .and_return() for method stubs. This method accepts multiple arguments, and can be implemented as follows:

context 'no decision has been made' do
  before do
    allow(Decider).to receive(:decision?).and_return(false, false, true)
  end

  it 'continues to loop' do
    expect(Decider).to receive(:decision?).exactly(3).times
    Decider.undecided
  end
end

I think this approach is fine in this simple case. We’ve tested that the loop continues when .decision? is false on two passes, but it bothers me a little that true is being passed to in effect bring the spec to a close. There is another simple way that doesn’t involve passing a loop-breaking value from the spec.

RSpec Mocks comes to the rescue again with .and_yield(), which can be chained together for multiple passes. It’s also convenient that we don’t necessarily have to pass an argument to .and_yield, as the breaking loop doesn’t yield anything. In combination with allow(), a setup that takes control of loop passes/termination can be written as:

before do
  allow(Decider).to receive(:loop).and_yield.and_yield
end

The context and expectation for the loop-breaking case is the same as before:

context 'a decision has been made' do
  before do
    allow(Decider).to receive(:decision?) { true }
  end

  it 'breaks the loop' do
    expect(Decider).to receive(:decision?).once
    Decider.undecided
  end
end

I like that .and_yield is allowed twice and the expectation is .decision? will only be called once. Additionally, the loop controlling before block is reusable for the alternative case, and I don’t have to pass the 3rd argument of true as in the previous approach:

context 'no decision has been made' do
  before do
    allow(Decider).to receive(:decision?).and_return(false, false)
  end

  it 'continues to loop' do
    expect(Decider).to receive(:decision?).exactly(:twice)
    Decider.undecided
  end
end

This approach also makes testing infinite loops cleanly viable. For example, we may want to ensure that an outer infinite loop behaves as expected on subsequent passes after an inner loop breaks. In this case, .and_yield() provides a way to do that without getting caught in an infinite loop when running specs.

Resources: RSpec Mocks, RSpec Receive Counts, Relevant Gist

''