M Bellucci
Posted on January 14, 2020
If I ask you to write a test for this function:
def sum(*nums)
nums.inject(:+)
end
your answer would be immediate
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:
#### 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.
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?
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?:
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?
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.
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
Posted on January 14, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.