The Ruby 2.5.0 feature nobody talks about

We have branch coverage now, and that’s great!

Alessandro Rodi
4 min readJan 16, 2018

Background

Since many years the ruby community asked to have branch coverage. That’s something that if you worked a little bit with JS frameworks you really missed. If you don’t miss it, you probably don’t know what is branch coverage and therefore you are in the right place.

Line coverage

When you execute your code, you can know which lines have been executed and which ones have not been executed. Lines coverage is what has (almost) always been available in ruby. Let’s take this example:

# hello.rb (with line numbers)1: def hello(number)
2: if number == 1
3: 'world'
4: else
5: 'mars'
6: end
7: end
8:
9: hello(1)

if we analyse the coverage by running:

Coverage.start
load 'hello.rb'
puts Coverage.result

we will obtain:

{"hello.rb"=>[1, 1, 1, nil, 0, nil, nil, nil, 1]}

which gives us the coverage for each line of our hello.rb file. Ones are for covered lines, nils are for syntax and white lines, zeros are for uncovered lines. We’ll consider ones and nils as covered (Y) and zeros as uncovered (N) for simplicity. Let’s see the result then:

Y -> 1: def hello(number)
Y -> 2: if number == 1
Y -> 3: 'world'
Y -> 4: else
N -> 5: 'mars'
Y -> 6: end
Y -> 7: end
Y -> 8:
Y -> 9: hello(1)

It correctly reports that we never executed the “mars” line and therefore that line is not covered. That’s because we executed thehello method only with parameter 1. If we would call also hello(2) we would see everything covered. Easy.

Line coverage reports which lines have been executed, and which have not been.

What happens if your code looks as follow?

# hello.rb1: def hello(number)
2: (number == 1) ? 'world' : 'mars'
3: end
4:
5: hello(1)

We have the exact same code but we are now using the ternary operator and therefore we have everything in one line. The coverage result will be:

{"hello.rb"=>[1, 1, nil, nil, 1]}

Y-> 1: def hello(number)
Y-> 2: (number == 1) ? 'world' : 'mars'
Y-> 3: end
Y-> 4:
Y-> 5: hello(1)

That’s the first issue with line coverage. Line coverage works on a “per-line level” and is absolutely not capable of understanding that we never went into the “mars” branch.

With branches we mean the two possible execution flows that and if block can have: they are the then and the else branches.

In our case the then branch is the one that returns “world”, the else branch is the one returning “mars”.

Our line coverage works great, but nothing tells us that the “mars” branch has never been executed. We clearly miss branch coverage.

Branch coverage

ruby 2.5.0 introduces branch coverage! That’s a great news because in the future, tools like simplecov will be able to report us exactly which branches of our code have been executed and which have not been.

Code coverage is mainly used in test suites, to report how much of the application code has been covered by tests. This means more precise numbers in the future and the assurance that all the branches of your code have been covered. That’s great!

In our example above, if we use ruby 2.5.0 and run

Coverage.start(branches: true)
load 'hello.rb'
puts Coverage.result

we will obtain:

"hello.rb"=> {
:branches=> {
[:if, 0, 2, 2, 2, 34]=> {
[:then, 1, 2, 18, 2, 25]=>1,
[:else, 2, 2, 28, 2, 34]=>0
}
}
}

which reports for each branch (then and else) their coverage. Each key is in the format

[clause, id, row_start, col_start, row_end, col_end] => covered 

and we can see that we have a else branch uncovered (0).

The hidden branch

Another example were the line coverage wouldn’t be enough is the following:

# hello.rb (with line numbers)1: def hello(number)
2: if number == 1
3: 'world'
4: end
5: end
6:
7: hello(1)

Can you guess why?

Line coverage would simply tell you that everything is covered and does not see that we never executed our code with a number different from 1.

hello(1) # returns “world”
hello(2) # returns nil

The new branch coverage will, instead, see perfectly theif block and give us the coverage for both branches:

"hello.rb=> {
:branches=> {
[:if, 0, 2, 2, 4, 5]=> {
[:then, 1, 3, 4, 3, 11]=>1,
[:else, 2, 2, 2, 4, 5]=>0
}
}
}

and tell us: “hey! you executed the hello method passing 1 but, what would happen if you pass something different?”

Conclusions

That’s the greatest feature ruby 2.5.0 introduced and it will still take some months before seeing a tool using it, but the ruby community has now branch coverage and that’s a great news.

Having a test suite with branch coverage 100% guarantees that you really consider all possible cases and execution flows of your code and reduces the possibility of bugs.

At Renuo we are all looking forward to integrate that in our test suites to increase the quality of our software even more. We always guaranteed 100% lines coverage with tests and this will allow us to push towards 100% branch coverage also for our ruby code! 🙌

--

--

Alessandro Rodi

Open Source Software Engineer at Renuo AG. Located in Zürich. I do stuff. Sometimes.