timacdonald/callable-fake
A tiny PHP testing utility for faking/invoking callables. CallableFake lets you replace closures or invokable objects, record calls and arguments, assert usage, and return configured values—useful for isolating behavior in PHPUnit/Laravel tests.
Installation:
composer require --dev timacdonald/callable-fake
Ensure your phpunit version is 10.x, 11.x, or 12.x (check composer.json requirements).
First Use Case: Replace a callable (e.g., a Closure, method, or static call) with a fake to capture invocations and assert behavior. Example:
use Timacdonald\CallableFake\CallableFake;
// Create a fake for a Closure
$fake = CallableFake::create(function () { return 'original'; });
// Replace the callable in your code (e.g., via dependency injection or manual swap)
$result = $fake(); // Captures the call
// Assert the fake was called
$fake->assertCalled();
Key Classes:
CallableFake: Core class for faking and capturing calls.CallableFakeResolver: Resolves return values (supports named returns via name()).CallableFakeException: Thrown on assertion failures.Where to Look First:
Faking a Callable:
Replace a Closure, method, or static call with a CallableFake instance.
// Fake a Closure
$fake = CallableFake::create(fn() => 'original');
$fake('arg1', 'arg2'); // Captures args
// Fake a method call (via dependency injection or mocking)
$service = new class($fake) {
public function __construct(private $callback) {}
public function execute() { return $this->callback(); }
};
Capturing Invocations: Access arguments, return values, and call count:
$fake->assertCalledWith('arg1', 'arg2'); // Assert args
$fake->assertCalledTimes(1); // Assert count
$fake->getArgs(); // Get all captured args
Return Value Resolution:
null unless configured.$fake->resolveWith('custom_return');
$fake->resolveWith(function () { return time(); }); // Dynamic
$fake->resolveWithName('success', 'custom_return'); // Named resolver
Assertions:
$fake->assertCalled();
$fake->assertNotCalled();
$fake->assertCalledWithConsecutive(
['arg1'], ['arg2'] // Assert calls in order
);
$fake->assertCalledIndex(0, 'arg1'); // Assert specific call index
Dependency Injection: Use the fake as a test double in Laravel’s container:
$this->app->bind(Closure::class, fn() => $fake);
Or via constructor injection in tests:
$fake = CallableFake::create(...);
$service = new Service($fake);
Laravel-Specific:
$fake = CallableFake::create(...);
event(new MyEvent());
$fake->assertCalled(); // Verify listener ran
$fake = CallableFake::create(...);
$middleware = new class($fake) implements Closure {
public function __construct(private $callback) {}
public function __invoke($request) { return $this->callback($request); }
};
Dynamic Fakes: Generate fakes dynamically for complex scenarios:
$fakes = collect(range(1, 5))->map(fn($i) => CallableFake::create(...));
Partial Mocking:
Combine with Laravel’s Mockery or PHPUnit’s MockObject for hybrid testing:
$mock = $this->mock(Service::class);
$mock->shouldReceive('callback')->andReturnUsing(
fn() => CallableFake::create(...)
);
PHPUnit Version Mismatch:
Class 'Timacdonald\CallableFake\CallableFake' not found.phpunit/phpunit is 10.x–12.x (see composer.json).composer why-not timacdonald/callable-fake to check constraints.Argument Capture Order:
getArgs() returns arguments in the order they were passed, not by parameter name.assertCalledWith() with exact args or named resolvers.Static Method Faking:
Helper::staticMethod()) requires manual binding.app()->bind():
$fake = CallableFake::create(...);
$this->app->bindStatic([Helper::class, 'staticMethod'], $fake);
Return Value Overrides:
resolveWithName() for conditional returns:
$fake->resolveWithName('success', 'data');
$fake->resolveWithName('error', new Exception());
Thread Safety:
CallableFake::reset().Assertion Failures:
getArgs() to inspect captured arguments:
var_dump($fake->getArgs()); // Debug actual calls
$fake->assertCalledTimes(2)->assertCalledWithConsecutive([...], [...]);
Lazy Evaluation:
resolveWith()) are lazy—they run only when the fake is invoked.getReturnValue() to inspect the resolved value after invocation.Laravel-Specific Debugging:
$this->app->forgetInstance(Closure::class);
Events::fake() alongside CallableFake to isolate event testing.Custom Resolvers:
Extend CallableFakeResolver for domain-specific logic:
class CustomResolver extends CallableFakeResolver {
public function resolve(): mixed {
return $this->name === 'api' ? $this->getApiResponse() : null;
}
}
$fake->setResolver(new CustomResolver());
Integration with Laravel Packages:
$fake = CallableFake::create(...);
$package->setCallback($fake); // Hypothetical package method
setCallback, onEvent).Performance:
$fake = CallableFake::create(...);
$this->fake = $fake; // Store for reuse
$fake->reset(); // Clear state between tests
Legacy Code:
composer require timacdonald/callable-fake:1.7.0
Named Resolvers:
$fake->resolveWithName('success', ['data' => 'value']);
$fake->resolveWithName('error', ['error' => 'failed']);
// Trigger based on args:
$fake->assertCalledWithName('success', ['arg1']);
How can I help you explore Laravel packages today?