php unit test code coverage with drive Pcov problems about function_exists

326 Views Asked by At

The coverage report showed two lines of if (!function_exists('function_name')) were not been covered, but these two functions within "if" was covered. It's ridiculous because when I used xdebug to be as the driver, these two lines were covered, but when I changed the driver to pcov without any other changes, the problem occurred.

if (!function_exists('birthday_to_age')) {

    /**
     * calculate the age of input date
     * @param Carbon $date input date
     * @return int the age
     */
    function birthday_to_age(Carbon $date): int
    {
        return floor((date('Ymd') - $date->format('Ymd')) / 10000);
    }
}


if (!function_exists('log_http_client_response')) {

    /**
     * Function log_http_client_response
     * @param Response $response
     * @param string $prefix
     * @param string $level
     */
    function log_http_client_response(Response $response, string $prefix = '', string $level = 'debug')
    {
        $content = $prefix . "Response http_status: {$response->status()}, http_headers: "
            . json_encode($response->headers()) . ", http_body: {$response->body()}";
        Log::$level($content);
    }
}

Did any one meet this problem? does any one know how to cover these two lines?

enter image description here

Thanks

2

There are 2 best solutions below

6
On

I had similar problem. I chose to ignore this line with this ignore rules so that it does not affect test coverage. Like this:

// @codeCoverageIgnoreStart
if (! function_exists('tmp_path')) { // @codeCoverageIgnoreEnd
    function tmp_path(string $fileName = ''): string
    {
        $path = 'tmp';

        return $fileName ? sprintf('%s/%s', $path, $fileName) : $path;
    }
}

Result: enter image description here

But I'm wondering how to get test coverage to mark this line as "passed" as well. My solution ensures that test coverage is not negatively affected.

0
On

While in your question you already present the answer (use Xdebug for coverage), a question that may remain interesting apart from that correct handling (use the right tool for the job), is why there are these differences.

My educated guess would be that this is related on how PHP works and that XDebug and Pcov are behaving differently.

So where could that be?

With your code at hand, we can see that functions are conditionally defined and as both code-coverage reports show, these conditionally defined functions are being executed.

Now we could assume that all PHP is being executed as otherwise these function definitions could not be covered by calling the functions.

That means, that the moment the PHP file is loaded, parsed, turned into bytecode and executed (for the first time during this loading), Xdebug captures the run while Pcov does not.

Only afterwards, the test-case can actually test those units (the functions) and in a sense more correctly, the if clauses should not be under coverage as they are not part of the units but support code of a dynamic function loading mechanism which you expect to be plain PHP and to work, therefore we don't test for it (this is similar to that we don't test third-party code, your mileage may vary as this is not entirely the same and you may want to have a loading test not a unit test, but then I'd say smoke testing suffices, no coverage needed).

Unless there is an option for Pcov or it's driver in Phpunit to come closer to XDebugs behaviour, it may help to pull the loading of that file into the phase where Pcov is already capturing coverage data (or Pcov may not cover execution during loading phase).

As IIRC Pcov still is a bit faster than XDebug3 in coverage mode only, this may be one of the reasons, therefore it could be that you're looking for a feature that it does not have. Just writing, so it's not creating the false impression I verified this, which I didn't.

Long story short: My educated guess is that this is related to PHP loading phase, remove that file from the autoloader and load it first during the test where you'd like to capture it.

Or do both, just add another include but during test-run, from the fragment you show, this should be a re-entrant safe PHP script inclusion as the function definitions are gated. The if clauses are line covered in both cases.

If that does not help (or you're not able to because you don't want to reconfigure the auto-loading for testing only), use the annotations.

IMHO, the annotations are legit because we do not have to cover the loading phase but instead calling that function. And we see in the coverage report that this worked.

The if clauses are only executed when that file is loaded. And that's how include/require(_once) works in PHP¹, afterwards only the definitions are available and the coverage driver relates to the lines in the file, but actually it's already byte codes in random access memory, the pathname of the file and the lines are only references. Meet the old friends __FILE__ and __LINE__.