Code coverage can be a flashlight or a fog machine. Shine it right and you see paths. Point it wrong and you walk into walls with a smile.
Teams brag about eighty percent. Some push for a hundred. Numbers look clean on a dashboard. Bugs are not that polite.
What does eighty percent actually say?
Line coverage says which lines ran. It does not say the lines were checked for truth. It does not say the test would fail if the line broke.
You can hit a line and assert nothing. That counts. The tool smiles. Your pager will not.
Line or branch or both?
Branch coverage watches the choices. If and else, case, short circuit tricks. Bugs love the path you did not take.
Chasing only lines makes tests pet the happy path. Add branch coverage and you start asking the right questions.
// Example where line coverage can lie
int discount(int value) {
if (value >= 100) return value - 10;
else return value; // what about negative values?
}
// A test that hits both lines but not both ideas
@Test public void discount_applies_for_large() {
assertEquals(90, discount(100)); // else branch still unchallenged
}Branch coverage would nudge you to try zero, small values, and odd cases.
Are we measuring code or behavior?
Coverage is a map of code executed. What we care about is a map of behavior verified. The first is easy to count. The second keeps users happy.
When a test fails, you feel it in your gut. When coverage drops, you feel it in a meeting. Pick the one that guards value.
Where do bugs hide?
They hide in parsing, in time zones, in off by one, in string trims, in IO retries. They hide where branches split.
Target tests to weird inputs, boundary values, race conditions, and failure codes. That is where coverage becomes sharp.
Can we gate merges without gaming the number?
Yes. Gate on coverage deltas, not global vanity. Guard changed files. Protect critical packages more than others.
Make the gate explain itself. When the bot talks about a risky line you just changed, you listen.
# Jenkins shell step example: fail if changed files drop coverage
CHANGED=$(git diff --name-only origin/main...HEAD | grep -E '\.(js|rb|java|py)$' || true)
for f in $CHANGED; do
./scripts/coverage_for_file.sh "$f" > /tmp/cov.txt
PCT=$(awk '/Lines:/ {print int($2)}' /tmp/cov.txt)
if [ "$PCT" -lt 70 ]; then
echo "Low coverage in $f: $PCT%"
exit 1
fi
done
echo "Coverage check passed"Hook this after your tests. Keep the threshold modest so folks do not write fake tests.
What tools make this easy right now?
Java feels good with JaCoCo. It reads branches. It plugs into Maven or Gradle and feeds Jenkins just fine.
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.6.3</version>
<executions>
<execution>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals><goal>report</goal></goals>
</execution>
</executions>
</plugin>Ruby has SimpleCov. It is one line in your test helper and gives a clean HTML report.
# test/test_helper.rb
require "simplecov"
SimpleCov.start do
add_filter "/test/"
minimum_coverage 75
endNode rides with Istanbul and mocha. It prints branches and statements and makes it easy to see holes.
npm install --save-dev istanbul mocha
istanbul cover ./node_modules/mocha/bin/_mocha -- -R spec "test/**/*.spec.js"How do we point coverage to the money paths?
Tag code by risk. Payments, auth, pricing, data writes. Set higher goals for those folders. Be kinder on glue and views.
Teach the bot about folders. Different floors for different rooms. You will feel less stress and ship with fewer regrets.
# SimpleCov a little stricter for core
SimpleCov.start do
add_filter "/spec/"
add_group "Core", "app/core"
add_group "Adapters", "app/adapters"
minimum_coverage 70
SimpleCov.at_exit do
SimpleCov.result.groups["Core"].covered_percent < 85 && abort("Core under 85")
end
endWhat about legacy code that bites back?
Wrap a seam. Add a pin test around the current behavior. Then refactor inside the fence. Grow tests from the edges inward.
You will not win the whole codebase in a sprint. Win a file. Then a module. Keep a scoreboard and celebrate small steps.
# Python example: pin current behavior before refactor
def calc_fee(plan, minutes):
if plan == "pro":
return max(0, minutes - 100) * 2
return minutes * 3 # odd but current reality
def test_pin_current_behavior():
assert calc_fee("basic", 1) == 3
assert calc_fee("pro", 100) == 0
assert calc_fee("pro", 101) == 2Should we chase one hundred?
One hundred is a training exercise, not a life goal. For tiny libs it is fine. For large apps it can turn into theater.
Better target is risk weighted. Keep the knife sharp where failure costs money or sleep. Allow room to move elsewhere.
How do we prevent fake tests?
Ask tests to prove a property, not to call a function. Write failures first. Delete an assertion on purpose and see if the test still passes. If it does, fix it.
Mutation testing can help. Flip a sign, swap a branch, tweak a constant, and see if tests yell. If they stay quiet, coverage lied.
# Ruby: try mutant for a focused class
# gem install mutant-rspec
mutant --include lib --require my_app --use rspec MyApp::PriceCalculatorRun this on core code only. It is slow but honest.
Can coverage help code review?
Yes. Show a diff view that highlights new lines without tests. Ask for one meaningful test per branch introduced. Keep the rule simple.
Services like Coveralls and Code Climate post comments on pull requests already. Use the comment as a guide, not a whip.
What small habits move the needle today?
Write the failing case first. Name tests by behavior. Keep one assert per idea. Test the boundary before the middle. Touch error handling with real errors.
Run tests in your editor. Let CI post the badge. Talk about flaky tests in standup and delete the flakes fast.
How does this tie into our current toolchain?
Jenkins is on many servers. Travis is helping a lot of open source work. GitHub pull requests are the new hallway. Put coverage checks where people already look.
Keep reports near the build. Link to the line. Make it easy to act on the data in the moment a change is fresh in mind.
Why call the metric a flashlight and not a scoreboard?
Scoreboards push people to pad stats. Flashlights help you find leaks. When coverage points to a dark corner and you add a test, you feel the value right away.
Use coverage to discover, not to decorate. That mindset changes meetings and code at the same time.
Quick checklist for coverage that informs
Track branches, not just lines. Gate by diff, not by total. Raise the bar on risky code. Mutate core logic once in a while. Keep tests honest and small.
Celebrate tests that fail for the right reason. They save more headaches than a green badge ever will.
Compact wrap
Coverage can trick you with big numbers or help you ship with calm. Point it at behavior, branches, and changes. Let the number guide craft, not pride.
Make coverage inform, not deceive. Your future self and your inbox will thank you.