Provides a small PHPUnit extension for mocking native PHP functions from tests.
The idea is based on Symfony PHPUnit Bridge's ClockMock and DnsMock: register a function in the tested namespace and
dispatch that function through a test-controlled mock. See
Symfony's Clock Mocking
and DNS-sensitive tests
documentation for the original pattern.
- PHP 8.1 or higher
- PHPUnit 10.5, 11.5, 12.5, 13, or higher
composer require --dev bizkit/phpunit-function-mockRegister the extension in your PHPUnit configuration:
<?xml version="1.0" encoding="UTF-8"?>
<phpunitbootstrap="vendor/autoload.php">
<extensions>
<bootstrapclass="Bizkit\PHPUnitFunctionMock\PHPUnitExtension" />
</extensions>
</phpunit>Use #[RegisterFunctionMock] on the test class or method to register the function hook. Configure the callable in the
test body with FunctionMock.
<?phpdeclare(strict_types=1);
namespaceApp\Tests\Service;
useApp\Service\TokenGenerator;
useBizkit\PHPUnitFunctionMock\Attribute\RegisterFunctionMock;
useBizkit\PHPUnitFunctionMock\FunctionMock;
usePHPUnit\Framework\TestCase;
#[RegisterFunctionMock('random_int')]
finalclass TokenGeneratorTest extends TestCase
{
publicfunctiontestGenerateToken(): void
{
FunctionMock::mock('random_int', staticfn (int$min, int$max): int => 1234);
self::assertSame('token-1234', newTokenGenerator()->generate());
}
}When the test suite is loaded, the extension registers a random_int() function in the test namespace and in the
matching application namespace with Tests removed. For example, App\Tests\Service\TokenGeneratorTest also registers
mocks for App\Service.
Pass a list when a test needs several functions:
#[RegisterFunctionMock(['file_exists', 'is_file'])]
finalclass FileLoaderTest extends TestCase
{
publicfunctiontestLoad(): void
{
FunctionMock::mockMany([
'file_exists' => staticfn (string$path): bool => true,
'is_file' => staticfn (string$path): bool => true,
]);
// ...
}
}#[RegisterFunctionMock] is repeatable, so the same class or method can declare multiple #[RegisterFunctionMock]
attributes.
Use class: when the tested code lives in a namespace that cannot be inferred from the test class name.
useApp\Service\TokenGenerator;
useBizkit\PHPUnitFunctionMock\Attribute\RegisterFunctionMock;
#[RegisterFunctionMock('random_int', class: TokenGenerator::class)]
finalclass TokenGeneratorTest extends TestCase
{
}The function is registered in the namespace of the class passed to class:.
You can also register functions without PHPUnit attributes:
useApp\Service\TokenGenerator;
useBizkit\PHPUnitFunctionMock\FunctionMock;
FunctionMock::register(TokenGenerator::class, 'random_int');
FunctionMock::mock('random_int', staticfn (int$min, int$max): int => 1234);Pass null to remove a single mock and restore native fallback for that function:
FunctionMock::mock('random_int', null);Mocks are global process state. The PHPUnit extension clears configured callables after annotated tests finish, error, or skip. If you configure mocks manually in unannotated tests, clear them yourself:
protectedfunctiontearDown(): void
{
FunctionMock::reset();
}This library relies on
PHP's fallback to the global namespace for functions.
In namespaced code, an unqualified function call such as random_int() first checks for a function in the current
namespace, then falls back to \random_int().
Because of that, mocks only work for unqualified function calls:
namespaceApp\Service;
random_int(1, 10); // can be mocked\random_int(1, 10); // cannot be mocked by this libraryThe namespaced function must also be registered before the target namespace calls that function for the first time. PHP
can cache the first function resolution in its literal cache, so if App\Service\random_int() does not exist yet and
PHP resolves the first call to \random_int(), later defining App\Service\random_int() may not affect that
already-compiled call site. See PHP bug #64346. This is why the attribute-based
extension registers functions when PHPUnit loads the test suite, before test methods execute.
Other limitations:
- Generated namespaced functions cannot be removed once declared. Only their configured callables are reset.
- Tests that register the same namespaced functions should run in separate processes when isolation matters.
This project follows Semantic Versioning 2.0.0.
Use the project's issue tracker to report bugs or request improvements.
See the LICENSE file for details (MIT).