PHPUnit: Can you exclude 3rd party libraries from having their notices converted to exceptions? EG with convertNoticesToExceptions

Viewed 43

I've been running PHPUnit with convertNoticesToExceptions turned on as best practice. https://phpunit.readthedocs.io/en/9.5/configuration.html#the-convertnoticestoexceptions-attribute

But of course, that's only appropriate for my own code: I'd prefer to have a different setting for (eg) vendor/. It's not as if I'm going to fix all the notices thrown by third party code.

The documentation has no suggestion that the settings can be different for different directories. (I vaguely remember reading about a way of ignoring notices from libraries, but might be wrong.)

I know about using @, but that solves a different problem.

It this just not an option?

1 Answers

For the sake of brevity, let's say that you have two classes in your project:

namespace MyNamespace;

class Foo
{
    public function myFunc(): bool
    {
        $i = [];
        $i["1"];

        return true;
    }
}
namespace MyNamespace;

class Bar
{
    public function myFunc(): bool
    {
        $i = [];
        $i["1"];

        return true;
    }
}

Both functions myFunc will trigger w warning (no clue what would trigger a notice in PHP 8.1), but we don't want to report it for the class Foo.

Your test would then look like this:

namespace MyNamespace\Tests;

use PHPUnit\Framework\TestCase;
use MyNamespace\Foo;
use MyNamespace\Bar;

class MyTest extends TestCase
{
    protected function setUp(): void
    {
        set_error_handler(static function (int $errorNr, string $errorStr, string $errorFile, int $errorLine, array $errorContext = []): bool {
            if (str_contains('/home/awons/Projects/test/src/Foo.php', $errorFile) && $errorNr === E_WARNING) {
                return true;
            }
            
            $phpUnitHandler = new \PHPUnit\Util\ErrorHandler(true, true, true, true);
            return $phpUnitHandler->__invoke($errorNr, $errorStr, $errorFile, $errorLine, $errorContext);
            
        });
    }

    protected function tearDown(): void
    {
        restore_error_handler();
    }

    public function testFoo(): void
    {
        $foo = new Foo();
        self::assertTrue($foo->myFunc());
    }

    public function testBar(): void
    {
        $bar = new Bar();
        self::assertTrue($bar->myFunc());
    }

    public function testFooAgain(): void
    {
        $foo = new Foo();
        self::assertTrue($foo->myFunc());
    }
}

This is not exactly the most performant solution and I would consider it a hack, but it should do the trick if that's what you need.

All you have to do is to make a decision about when you want to trigger the PhpUnit's error handler and when you want to ignore the notice. You can replace /home/awons/Projects/test/src/Foo.php with /vendor/ (or anything more specific to be 100% sure you got the correct path).

Also, I'm not sure how to get the current settings for the error handler. You might need to parse the config file for that or just move everything to a base class and hardcode values there (just keep them in sync with the actual settings).

::Edit:: There is even a way not to instantiate the PhpUnit's handler but just get it from the current context. All we have to do is to register an empty error handler and restore it immediately. Registering a new error handler returns the previous one. Assuming the base class is in ./tests you can build the following abstract test case:

use PHPUnit\Framework\TestCase;

abstract class BaseTestCase extends TestCase
{
    protected function setUp(): void
    {
        $phpUnitErrorHandler = set_error_handler(function () {
        });
        restore_error_handler();
        set_error_handler(
            static function (
                int $errorNr,
                string $errorStr,
                string $errorFile,
                int $errorLine,
                array $errorContext = []
            ) use ($phpUnitErrorHandler): bool {
                $vendorDir = realpath(__DIR__ . "/../vendor/");
                if (str_contains($vendorDir, $errorFile) && $errorNr === E_NOTICE) {
                    return true;
                }
                return call_user_func_array(
                    $phpUnitErrorHandler,
                    [$errorNr, $errorStr, $errorFile, $errorLine, $errorContext]
                );
            }
        );
    }

    protected function tearDown(): void
    {
        restore_error_handler();
    }
}
Related