PHPUnit CodeCoverage

Posted: 2019-01-09 17:47:16

PHPUnit CodeCoverage

PHPUnit CodeCoverage

There is a ton of information about your code PHPUNit CodeCoverage can provide. At the root just making sure your coverage is at a certain level and if not where is it lacking.

With it's xml output I use a script found here to check during our TravisCI builds if the code is meeting a required level.

Here is a gist of that script here and here is the .travis.yml section snippet that will run the coverage generating test and then review it for percentage of coverage.

script:
  - vendor/bin/phpcs --standard=psr2 app/
  - set -e
  - vendor/bin/phpunit --coverage-clover clover.xml --stop-on-failure --testsuite=Unit,Feature
  - php ./coverage-checker.php clover.xml 70

This is what we run in Travis to review the coverage, if it is below 70% it will fail.

If your unit tests cover 100% of your code, you're doing it wrong. Just my 2 cts. - Fabien Potencier

Setting Up

This can be tricky you need Xdebug in place else you might get "Coverage driver missing" errors.

Once ready you need to update your phpunit.xml here is my example:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit 
    /// left out some area for brevity
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>

        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
            <exclude>
                <directory suffix=".php">app/Http/Controllers/Auth</directory>
                <file>app/Console/Commands/SetupDeploymentCommand.php</file>
                <file>app/Console/Kernel.php</file>
                <file>app/Http/Kernel.php</file>
                <file>app/Providers/AppServiceProvider.php</file>
                <file>app/Providers/AuthServiceProvider.php</file>
                <file>app/Providers/EventServiceProvider.php</file>
                <file>app/Providers/BroadcastServiceProvider.php</file>
                <file>app/Providers/RouteServiceProvider.php</file>
                <file>app/Http/Controllers/Controller.php</file>
                <file>app/Exceptions/Handler.php</file>
                <file>app/Http/Middleware/Authenticate.php</file>
                <file>app/Http/Middleware/EncryptCookies.php</file>
                <file>app/Http/Middleware/RedirectIfAuthenticated.php</file>
                <file>app/Http/Middleware/TrimStrings.php</file>
                <file>app/Http/Middleware/TrustProxies.php</file>
                <file>app/Http/Middleware/VerifyCsrfToken.php</file>
            </exclude>
        </whitelist>
    </filter>
    /// left out some area for brevity
</phpunit>

Above I setup filter to exclude a bunch of Laravel files I do not want to maintain.

Running the Command

vendor/bin/phpunit --coverage-clover clover.xml --stop-on-failure  --coverage-html=build

This will output the clover.xml and make a folder called build make sure to include that in your .gitignore

On my Mac, when done, I just type open build/index.html and I go to a page like this

Exploring

As seen above I get a list of all my classes and how the coverage is on each them. It gets even better from here.

Class Level

So you are wondering why a class is missing coverage? Click on the Class folder and start to dig in.

Maybe it finds something you forgot or maybe it is fussing about a bit of code you know you tested?

In this example it could be either. So I have two main options.

Annotate the test

Let's say I have a Controller level test that is testing this:

    /**
     * @covers App\Http\Controllers\AdminController::index
     */
    public function testWorksAsAdmin()
    {
        $user = factory(\App\User::class)->states('admin')->create();
        $this->be($user);
        $this->get("/admin")->assertStatus(200)->assertSee("Show user");
    }

    /**
     * @covers App\Http\Controllers\AdminController::index
     */
    public function testWorksAsNonAdmin()
    {
        $user = factory(\App\User::class)->create();
        $this->be($user);
        $this->get("/admin")->assertStatus(302);
    }

You can see I specifically tell PHPUnit CodeCoverage that this controller tests are testing this area. But I do not let it know about the MiddleWare so I could do:

    /**
     * @covers App\Http\Controllers\AdminController::index
     * @covers App\Http\Controllers\AdminController::__constructor
     */
    public function testWorksAsAdmin()
    {
        $user = factory(\App\User::class)->states('admin')->create();
        $this->be($user);
        $this->get("/admin")->assertStatus(200)->assertSee("Show user");
    }

    /**
     * @covers App\Http\Controllers\AdminController::index
     * @covers App\Http\Controllers\AdminController::__constructor
     */
    public function testWorksAsNonAdmin()
    {
        $user = factory(\App\User::class)->create();
        $this->be($user);
        $this->get("/admin")->assertStatus(302);
    }

Then run the test command again and:

Annotate the Class

In the above I could have just done this:

    /**
     * @codeCoverageIgnore
     */
    public function __construct()
    {
        $this->middleware('admin_only')->only("index");
    }

And that is fine, as long as you know that middleware is tested in it's own area.

Dashboard

This is where some other nice info is surfaced about your code.

Should bring you to this layout

The CRAP rating is "interesting" you can read more about it here since I do not fully understand it myself but imo it comes down not to 100% coverage but:

  • Keep classes small and focused
  • Keep classed down to 1 public method, even Controllers letting other protected/private methods do the work
  • Test the class for message in and message out

"You don't send messages because you have objects, you have objects because you send messages - Sandi Metz Poodr Book"

Helpers

Two other tools to compliment that above

PHPMetrics

This tool http://www.phpmetrics.org/ can easily be setup and run showing more details about your classes and their state of "complexity" etc.

LaraStan

This tool https://github.com/nunomaduro/larastan when it does not go overboard in it's reading your code (set it to a low level) can do a good job of finding issues in your code that you did not find in your tests. It runs over your code including exceptions you did not cover in your tests etc. Really does find some interesting things.