avris/function-mock
Mock PHP functions in unit tests without changing your code. avris/function-mock lets you replace and control built-in and user-defined function calls at runtime, making it easier to test legacy code, edge cases, and hard-to-isolate behavior.
Install the Package:
composer require avris/function-mock --dev
No Laravel-specific configuration is needed; the package works as a standalone utility.
Basic Usage in Tests:
Initialize the mock in your test class's setUp() method (PHPUnit) or beforeEach() (Pest):
use Avris\FunctionMock\FunctionMock;
public function setUp(): void
{
$this->mock = new FunctionMock();
$this->mock->mock('str_replace', function ($search, $replace, $subject) {
return "mocked-$subject";
});
}
public function tearDown(): void
{
$this->mock->unmock('str_replace');
}
First Test Case:
Test a function that relies on str_replace without hitting the real implementation:
public function test_string_replacement()
{
$result = str_replace('foo', 'bar', 'hello foo world');
$this->assertEquals('mocked-hello foo world', $result);
}
Where to Look First:
src/FunctionMock.php (single class, ~100 lines).mock(string $functionName, callable $callback): Replace a function.unmock(string $functionName): Restore original function.isMocked(string $functionName): Debugging helper.Replace unreliable or external-dependent functions (e.g., file_get_contents, time):
$mock = new FunctionMock();
$mock->mock('file_get_contents', fn($url) => '{"status": "mocked"}');
$mock->mock('time', fn() => 1625097600); // Freeze time to 2021-06-30
Limit mocks to specific namespaces to avoid collisions:
$mock->mockInNamespace('App\\Services', 'log_error', fn($message) => null);
// Only affects calls from `App\Services\*`.
Use closures for dynamic return values or side effects:
$mock->mock('rand', function ($min, $max) {
static $counter = 0;
return $counter++ % 2 === 0 ? 10 : 20; // Alternate between 10 and 20
});
Combine with Laravel’s testing tools (e.g., HTTP tests):
public function test_api_with_mocked_time()
{
$mock = new FunctionMock();
$mock->mock('time', fn() => 1700000000); // 2023-11-15
$response = $this->get('/api/time-sensitive');
$mock->unmock('time'); // Cleanup
$response->assertJson(['date' => '2023-11-15']);
}
For user-defined globals (e.g., config() in Laravel), wrap them in a class first or use runtime overrides:
// Avoid: Mocking `config()` directly (not supported).
// Instead: Refactor or use Laravel’s service container mocking.
Target Laravel’s global helpers (e.g., config(), app()) by mocking their underlying functions:
$mock->mock('app', fn($make = null) => $this->app); // Redirect to Laravel container
Test providers that rely on global functions (e.g., file_exists):
public function test_provider_registration()
{
$mock = new FunctionMock();
$mock->mock('file_exists', fn($path) => true);
$this->app->register(ServiceProvider::class);
// Test logic...
$mock->unmock('file_exists');
}
Mock functions called by Artisan commands (e.g., date for timestamped logs):
public function test_command_with_fixed_time()
{
$mock = new FunctionMock();
$mock->mock('date', fn($format) => '2023-01-01');
$this->artisan('command:log')
->expectsOutput('2023-01-01');
$mock->unmock('date');
}
Mock functions in event listeners (e.g., json_encode for payloads):
public function test_event_listener()
{
$mock = new FunctionMock();
$mock->mock('json_encode', fn($data) => '{"mocked": true}');
$this->dispatch(new Event());
// Assert mock was called...
$mock->unmock('json_encode');
}
Simulate external API calls in middleware:
public function test_middleware_with_mocked_api()
{
$mock = new FunctionMock();
$mock->mock('file_get_contents', fn($url) => '{"status": "ok"}');
$response = $this->get('/api/protected');
$response->assertJson(['status' => 'ok']);
$mock->unmock('file_get_contents');
}
Use closures to log or assert function invocations:
$calls = [];
$mock->mock('log_error', function ($message) use (&$calls) {
$calls[] = $message;
return true;
});
// Later...
$this->assertCount(1, $calls);
Return different values based on input:
$mock->mock('str_contains', function ($haystack, $needle) {
return $needle === 'error' ? false : true;
});
Simulate function failures:
$mock->mock('json_decode', function ($json) {
throw new \RuntimeException('Invalid JSON');
});
Useful for one-off tests or callbacks:
$mock = new FunctionMock();
$mock->mock('time', fn() => 1625097600);
$result = tap(function () use ($mock) {
return strftime('%Y', time()); // Returns "2021"
}, fn() => $mock->unmock('time'));
Override functions used by model factories:
public function test_factory_with_mocked_uuid()
{
$mock = new FunctionMock();
$mock->mock('ramsey\uuid\Uuid::uuid4', fn() => 'mocked-uuid');
$user = User::factory()->create();
$this->assertEquals('mocked-uuid', $user->uuid);
$mock->unmock('ramsey\uuid\Uuid::uuid4');
}
Namespace Collisions:
str_replace in a global scope may interfere with custom str_replace functions in namespaced classes.mockInNamespace() to scope mocks:
$mock->mockInNamespace('App\\Helpers', 'str_replace', ...);
Forgotten unmock():
unmock() in tearDown() or use a context manager:
$mock = new FunctionMock();
$mock->mock('time', fn() => 1625097600);
try {
// Test logic...
} finally {
$mock->unmock('time');
}
Static Method Confusion:
User::generateToken()).Mockery or partialMock for static methods.Performance Overhead:
How can I help you explore Laravel packages today?