Dealing with untestable code
Tags: ruby, tests, rspec
Published: 2020-01-14 20:51:33

---
title: Dealing with untestable code
published: true
description: An introduction to testing mindset
tags: ruby, tests, rspec
---

If I ask you to write a test for this function:
```ruby
def sum(*nums)
  nums.inject(:+)
end
```
your answer would be immediate
```ruby
describe '#sum' do
 it { expect(sum(1,2,3,4)).to eq(10) }
end
```

But what happens when I ask you to write a test for this code:
```ruby
#### Print a rand number every three seconds ####
while sleep(3) do
  puts rand(10)
end
```

Stop for a moment and try to write a test for it.

You may face these difficulties:

1) This code never ends (the test will never end)
2) There are no inputs (how do I think of different cases?)
3) There are no outputs (I should assert against what?)
4) How do we execute this code?

For each of these points I would say:

1) The test must make it finish
2) False, we need to recognize them
3) False, we need to recognize them
4) We can wrap the code into a function

## Generalizing our understanding of input/output

**Input:** Any data that can affect the result of the execution.
* Read from database
* Get data from an external source (api-call)
* Return value from a collaborator (Time.now, File.read, I18n.locale)
* Parameters
* Shared variables (inside an object could be the instance variables)

**Output:** 
* Return value
* Call collaborator
* Side effects
  * Http call
  * Write to a Database
  * Shared variables mutated
  * Write to a file
  * Internal state mutation

## Recognizing input/output

Outputs:
In the first example, the output is a return value.
In the second example, the output is a call to a collaborator `self.puts`.

Inputs:
In the first example, the input values are the parameters.
In the second example, the input values come from a call to a collaborator `self.sleep(3)` and `self.rand(10)`.

In the first example, the input values are passed by parameter by the caller while in the second example, the input values are gathered by the code itself.

So in order to state multiple inputs' combinations, we need to mock the input gathering part of the code.

```ruby
def execute
  while sleep(3) do
    puts rand(10)
  end
end

require 'rspec/autorun'

describe '#execute' do
  it 'prints a random number' do
    # Pending: Simulate input gathering
    execute
    # checking the output
    expect(self).to have_received(:puts)
  end
end
```

## How can we simulate the input-gathering part?

```ruby
def execute
  while sleep(3) do
    puts rand(10)
  end
end

require 'rspec/autorun'

describe '#execute' do
  it 'prints a random number' do
    allow(self).to receive(:sleep).and_return(true, true, false)
    rand1, rand2 = 10, 20
    allow(self).to receive(:rand).and_return(rand1, rand2)

    execute

    expect(self).to have_received(:puts).with(rand1).ordered
    expect(self).to have_received(:puts).with(rand2).ordered
  end
end
```

## Leason learned

Why the second case wasn't trivial to test?
it is because input/output is not easy to recognize.
Would it be easier to write a test if we make it obvious by passing them as parameters?

Think now, how would you test this code?:

```ruby
def execute(loop_condition, next_number, print_callback)
  while loop_condition.() do
    print_callback.(next_number.())
  end
end

execute(->{sleep(3)}, ->{rand(10)}, ->(x){puts(x)})
```

What do you think, is it easier to test?


```ruby

require 'rspec/autorun'

describe '#execute' do
  it 'prints a random number' do
    condition = double; allow(condition).to receive(:call).and_return(true, false)
    next_number = double(call: 1234)
    print = double(call: nil)

    execute(condition, next_number, print)

    expect(print).to have_received(:call).with(1234)
  end
end
```

## Isolating side effects into collaborator objects

Instead of isolating side effects behind Procs let's use Objects which will act as collaborators to the execute method.

```ruby
  interval = Class.new { def wait; sleep(3); end  }.new
  numbers_stream = Class.new { def next; rand(10); end  }.new
  screen = Class.new { def print(n); puts(n); end }.new
 
  def execute(interval, numbers_stream, screen)
    while interval.wait do
      screen.print(numbers_stream.next)
    end
  end
 
  require 'rspec/autorun'
 
  describe '#execute' do
    it 'prints a random number' do
      interval = double
      allow(interval).to receive(:wait).and_return(true, true, false)
 
      numbers = double
      n1, n2 = 10, 20
      allow(numbers).to receive(:next).and_return(n1, n2)
 
      screen = double('screen', print: nil)
      execute(interval, numbers, screen)
 
      expect(screen).to have_received(:print).with(n1).ordered
      expect(screen).to have_received(:print).with(n2).ordered
    end
  end
```