Coverage
Starting coverage
Coverage has to start before the code under test is loaded, or the lines are already compiled without the counter. The test file starts it first, then requires the library:
require "coverage"
Coverage.start(lines: true)
require_relative "../lib/calculator"
require "minitest/autorun"
Coverage.start takes the kinds of coverage: lines, branches, methods and oneshot_lines. Line coverage is the cheap default; branch coverage answers the harder question of whether each side of an if ran.
The report
Coverage.result returns a hash keyed by file. Each value has a :lines array with one entry per line: nil for a line that cannot run, zero for a line that did not, a positive count for a line that did. The summary is four lines of arithmetic:
Minitest.after_run do
Coverage.result.each do |file, data|
next unless file.end_with?("lib/calculator.rb")
lines = data[:lines]
covered = lines.count { |n| n && n.positive? }
total = lines.count { |n| !n.nil? }
puts format("%s: %d/%d lines (%.0f%%)", File.basename(file), covered, total,
covered * 100.0 / total)
end
end
The filter on the file name matters: a report that includes the test file and every gem reports a number that means nothing. A project reports the files it owns.
Running it
ruby test/coverage_test.rb
Finished in 0.000829s, 1206.7224 runs/s, 1206.7224 assertions/s. 1 runs, 1 assertions, 0 failures, 0 errors, 0 skips calculator.rb: 3/5 lines (60%)
The test only calls add, so the two lines of divide and its guard are uncovered and the percentage is honest. Add a test for the zero case and the same report moves to 100% — which is the moment to read the next section.
Reading it honestly
- A covered line is not a checked line. A test that calls a method and asserts nothing still counts the line.
- 100% is a floor, not a goal. The last few percent are often error branches that are hard to reach; a team that chases them writes tests of the test.
- Branch coverage is the better number. Line coverage says the line ran; branch coverage says both outcomes of the condition ran.
- New code is the useful target. A coverage gate on a changed file catches the untested branch a commit adds; a gate on the whole repository blocks the first refactor.
What to watch for
Starting coverage after a require misses the code
that was already loaded, which is why the order in the first block is not
cosmetic.
A coverage run in the same process as a profiler or a reloader measures
the tool as well as the code. Run coverage in its own invocation, or the
numbers drift between machines.
Next: RSpec in one lesson
Article author: Arthur Isaev