Introduction
It’s a common practice to measure the quality of tests to make sure they cover all the code. Most ruby developers are familiar with SimpleCov. By default, it measures only line coverage, which sometimes can make false assurance that our code is fully tested. In Ruby we have many options to write conditional code in single lines:
return if something_happened?
some_var = argument.is_a?(SomeClass) ? argument : SomeClass.new(argument)
In such cases, line coverage will mark lines as covered no matter what’s the result of the condition.
Line coverage
Let’s take a look at a very simple example:
def read_file_if_exists(path)
return '' unless File.exist?(path)
File.read(path)
end
and spec for it:
require 'simplecov'
SimpleCov.start
require_relative './example.rb'
RSpec.describe 'read_file_if_exists' do
subject { read_file_if_exists('example.txt') }
it 'returns content of the file' do
expect(subject).to eq('abc')
end
end
As you’ve probably noticed I haven’t tested the case when a file doesn’t exist. Yet my line coverage is 100%.
$ rspec example_spec.rb
.
Finished in 0.0014 seconds (files took 0.07227 seconds to load)
1 example, 0 failures
Coverage report generated for RSpec to /Users/kukicola/Desktop/example/coverage. 3 / 3 LOC (100.0%) covered.
My test isn’t perfect but I think we can agree that in this case, it’s not the end of the world.
Let’s move on to the second example:
def number_to_dollar(number)
dollar_part = "#{number.to_i} dollar(s)"
cent_part = number % 1 != 0 ? " #{((number - number.to_i) * 100).to_i} cent(s)" : nil
dollar_part + cent_part
end
and spec for it:
require 'simplecov'
SimpleCov.start
require_relative './example2.rb'
RSpec.describe 'number_to_dollar' do
subject { number_to_dollar(2.56) }
it 'returns dollar and cents' do
expect(subject).to eq('2 dollar(s) 56 cent(s)')
end
end
My test will pass and line coverage is 100%.
If you look carefully you’ll see that my code has an obvious bug.
For values without a decimal part it will raise an error because it won’t be able to add nil
to the string:
irb(main):002:0> number_to_dollar(2)
/Users/kukicola/Desktop/example/example2.rb:4:in `+': no implicit conversion of nil into String (TypeError)
What is branch coverage and why it’s important?
Branch coverage is a measure of how many branches/results of the conditions have been executed during testing.
You can turn it on by simply adding enable_coverage :branch
into SimpleCov.start
block.
Let’s enable it for my spec:
require 'simplecov'
SimpleCov.start do
enable_coverage :branch
end
require_relative './example2.rb'
RSpec.describe 'number_to_dollar' do
subject { number_to_dollar(2.56) }
it 'returns dollar and cents' do
expect(subject).to eq('2 dollar(s) 56 cent(s)')
end
end
In my coverage/index.html
file I can see that my test doesn’t cover all the branches:
1 files in total.
5 relevant lines, 5 lines covered and 0 lines missed. ( 100.0% )
2 total branches, 1 branches covered and 1 branches missed. ( 50.0% )
Thanks to branch coverage it’s easy to identify that I missed a case when the cent part is zero. Having that information I can add additional test for that case, see that it fails and fix my code.
Conclusion
As you can see even 100% line coverage is not enough to be sure that my code is correct and fully tested. Branch coverage can help to identify missed cases and prevent potential bugs in the app.