Skip to Content
Course content

168: Code Coverage Reports with PHPUnit

Click on the "Edit" button in the top corner of the screen to edit your slide content.

By now, you're probably comfortable writing tests. You know that when the green bar appears, your code is doing what you expected. But there's a lingering question that always haunts me after a long coding session: What did I forget to test?

That's where code coverage comes in. It doesn't tell you if your code is "correct"—it just tells you which lines of your code were actually executed during your test suite. It's a map of your blind spots.

Building a Discount Calculator with some blind spots

Let's build a small DiscountCalculator. This is a classic scenario because business logic usually involves a lot of "if this, then that" branches, which are exactly where bugs hide.

<?php
namespace App;

class DiscountCalculator {
    public function applyDiscount(float $total, string $customerType): float {
        if ($customerType === 'VIP') {
            return $total * 0.80; // 20% off
        }

        if ($total > 100) {
            return $total * 0.90; // 10% off for big spenders
        }

        if ($customerType === 'NEW') {
            return $total * 0.95; // 5% off for new users
        }

        return $total;
    }
}

Now, I'll write a quick test. I'm feeling lazy, so I'm only going to test the VIP scenario and the default case. I'm intentionally ignoring the "big spender" and "new user" logic.

<?php
use PHPUnit\Framework\TestCase;
use App\DiscountCalculator;

class DiscountCalculatorTest extends TestCase {
    public function testVipDiscount() {
        $calc = new DiscountCalculator();
        $this->assertEquals(80, $calc->applyDiscount(100, 'VIP'));
    }

    public function testNoDiscount() {
        $calc = new DiscountCalculator();
        $this->assertEquals(50, $calc->applyDiscount(50, 'REGULAR'));
    }
}

Generating the first coverage report

If I run phpunit now, everything passes. I might feel great, thinking my code is fully tested. But let's run it with the coverage flag. Assuming you have Xdebug or PCOV installed (you'll need one of these for coverage to work), I'll run:

vendor/bin/phpunit --coverage-html reports

This tells PHPUnit to analyze the execution and dump a pretty HTML report into a folder called reports. When I open index.html in my browser, I'll see a percentage. It won't be 100%. If I click through to the DiscountCalculator file, I'll see the VIP and default return lines highlighted in green, but the "big spender" and "new user" blocks highlighted in red.

The "Empty Report" mistake

Here's something that happened to me recently. I ran the coverage command on a new project and got a report that claimed 0% coverage, even though my tests were passing. I spent ten minutes questioning my sanity before I realized the problem: I hadn't told PHPUnit which files should actually be tracked.

By default, PHPUnit doesn't just guess which folders contain your source code. I had to add a block to my phpunit.xml file. If you see an empty report, check your config. It should look something like this:

<phpunit>
    <source>
        <include>
            <directory suffix=".php">src</directory>
        </include>
    </source>
</phpunit>

Once I added that, the report immediately populated. Lesson learned: always check your phpunit.xml before blaming the tool.

Closing the gaps

Now that the report has pointed out exactly where I'm vulnerable, I can write targeted tests to hit those red lines. I don't have to guess; I just follow the map.

public function testBigSpenderDiscount() {
    $calc = new DiscountCalculator();
    $this->assertEquals(180, $calc->applyDiscount(200, 'REGULAR'));
}

public function testNewCustomerDiscount() {
    $calc = new DiscountCalculator();
    $this->assertEquals(95, $calc->applyDiscount(100, 'NEW'));
}

Running the report again, those red blocks turn green. We've hit 100% coverage. Keep in mind, this doesn't mean the code is bug-free—it just means every line was executed at least once. You still need to think about edge cases, like negative totals or empty strings, but coverage ensures you didn't simply forget a whole chunk of logic.




📋 Practical Task

Achieving Full Coverage for the OrderTaxCalculator

You have been handed a OrderTaxCalculator class that handles different tax rates based on the region (USA, EU, and Others). However, the previous developer left the project without writing a full test suite.

Your Task:

  1. Create the OrderTaxCalculator class with the following logic:
    • If region is 'USA', tax is 10%.
    • If region is 'EU', tax is 20%.
    • If the order total is over 1000 regardless of region, apply an additional 'luxury tax' of 5% on top of the regional tax.
    • For any other region, tax is 5%.
  2. Write a PHPUnit test class. Initially, only write tests for the 'USA' and 'EU' basic rates.
  3. Configure your phpunit.xml to include your source directory.
  4. Run the code coverage report (--coverage-html reports) and identify which lines are uncovered (the luxury tax logic and the 'Other' region logic).
  5. Write the remaining tests until your coverage report shows 100% for the OrderTaxCalculator class.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.