Code coverage — percentage of lines/branches executed by tests
Beginner5 min read·eng-17-013
interview
Concept
Code coverage — a measurement of how much of the source code is executed when tests run. Usually expressed as a percentage of lines, branches, or statements tested.
Types of coverage:
- Line coverage: Percentage of code lines executed by tests.
- Branch coverage: Percentage of conditional branches taken (every
if/elsepath tested). - Statement coverage: Similar to line, but counts statements (multiple per line possible).
- Function/method coverage: Percentage of methods called by tests.
- Path coverage: Every possible execution path — exponential complexity, rarely practical.
How to measure in PHP: Xdebug or PCOV extension + PHPUnit's --coverage-html flag. Generates an HTML report showing which lines were/weren't executed.
Misuse of coverage metrics:
- High coverage ≠ good tests.
assertEquals(true, true)gives coverage but tests nothing. - 100% coverage ≠ no bugs. You can test every line with wrong assertions.
- Coverage measures WHAT was run, not THAT it was tested correctly.
What coverage IS useful for:
- Finding untested code paths. A 0% coverage area might be dead code or a forgotten feature.
- A baseline requirement for pull requests (e.g., "must have 80% coverage").
- Identifying gaps after adding features.
Branch coverage is more meaningful than line coverage: Testing both if and else branches is more thorough than just testing one path.
Good targets: 80%+ line coverage is a commonly cited threshold. Critical business logic should aim for 95%+. Generated code and trivial getters can be excluded.
Code Example
bash
# Run PHPUnit with coverage report
php artisan test --coverage
# Or with specific report format:
./vendor/bin/phpunit --coverage-html coverage-report/
# Open coverage-report/index.html in browser
# Require minimum coverage (CI):
./vendor/bin/phpunit --coverage-clover coverage.xml
# Then in CI: check that lines-covered/lines-valid > 0.80php
<?php
// HIGH COVERAGE but POOR TEST — covers the line but doesn't actually test behavior
class TaxCalculatorTest extends \PHPUnit\Framework\TestCase
{
public function test_tax_function_runs(): void
{
$calc = new TaxCalculator();
$result = $calc->calculate(100.00, 0.20);
$this->assertIsFloat($result); // just checks it returns a float — coverage: 100%, usefulness: low
}
}
// GOOD COVERAGE with MEANINGFUL TESTS
class TaxCalculatorTest extends \PHPUnit\Framework\TestCase
{
private TaxCalculator $calc;
protected function setUp(): void { $this->calc = new TaxCalculator(); }
public function test_calculates_20_percent_tax(): void
{
$this->assertEquals(20.00, $this->calc->calculate(100.00, 0.20));
}
public function test_rounds_to_two_decimal_places(): void
{
$this->assertEquals(3.33, $this->calc->calculate(9.99, 0.33333));
}
public function test_returns_zero_for_zero_rate(): void
{
$this->assertEquals(0.00, $this->calc->calculate(100.00, 0.0));
}
public function test_throws_for_negative_rate(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->calc->calculate(100.00, -0.1);
}
}
// BRANCH COVERAGE example — test both branches of an if statement
function classify(int $age): string
{
if ($age < 18) return 'minor'; // ← branch 1
elseif ($age < 65) return 'adult'; // ← branch 2
else return 'senior'; // ← branch 3
}
// Branch coverage requires testing ALL three branches:
class ClassifyTest extends \PHPUnit\Framework\TestCase
{
public function test_minor(): void { $this->assertEquals('minor', classify(17)); }
public function test_adult(): void { $this->assertEquals('adult', classify(30)); }
public function test_senior(): void { $this->assertEquals('senior', classify(65)); }
}
// Line coverage might be 100% with just test_adult(), but branch coverage requires all three
// Excluding from coverage
/** @codeCoverageIgnore */
class AutoGeneratedMigration { /* boilerplate */ }
// phpunit.xml — coverage config
/*
<coverage>
<include>
<directory suffix=".php">./app</directory>
</include>
<exclude>
<directory>./app/Console</directory> <!-- generated Artisan commands -->
</exclude>
<report>
<html outputDirectory="build/coverage"/>
</report>
</coverage>
*/