Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Fumocker Laravel Package

fp/fumocker

Fumocker is a lightweight PHP library for mocking function calls in unit tests. It helps you stub built-in or namespaced functions, isolate code from side effects, and write more predictable tests without introducing heavy testing dependencies.

View on GitHub
Deep Wiki
Context7

Getting Started

Fumocker is a dev-only seam for the one thing Laravel's test toolbox can't reach: a raw PHP function called directly inside legacy code. Carbon::setTestNow(), Http::fake(), Storage::fake(), and Bus::fake() cover injected/mockable collaborators — Fumocker covers time(), microtime(), rand(), uniqid(), file_get_contents(), curl_exec() being called inline in a class you can't refactor this sprint.

  1. Install as a dev dependency only:
    composer require --dev fp/fumocker
    
  2. Reconcile the constraint before anything else. The package predates PHP 7 and its composer.json targets PHP 5.x. On a Laravel 9/10/11 app running PHP 8.1+ Composer will likely refuse to resolve it. Decide up front: run that legacy test suite on a separate PHP 5.6/7.x image, or fork-and-patch the version constraint and own the fork. Discovering this in CI is the worst outcome.
  3. Where to look first. There is no docs site and effectively no README. Read the source in this order:
    • the Fumocker entry class — register() / unregister(), the entire public surface;
    • the Scope classes — these decide which files see the mock, and they're the part you'll misconfigure;
    • tests/ — the fixtures are the only real documentation.
  4. First use case. A billing/subscription service with hard-coded time:
    use Fumocker\Fumocker;
    
    $fumocker = new Fumocker();
    $fumocker->register('time', fn() => 1700000000);
    // exercise the code under test
    $fumocker->unregister('time');
    
    Confirm you actually need it: check the class under test. If it type-hints a collaborator, or the function call sits behind a contract you can bind in the container, use a normal mock. Fumocker is for the $this->expiresAt < time() line, nothing more.

Implementation Patterns

Wrap it in a base TestCase — never call register/unregister inline in tests. Cleanup must survive an assertion failure:

use Fumocker\Fumocker;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class LegacyTestCase extends BaseTestCase
{
    protected Fumocker $fumocker;

    protected function setUp(): void
    {
        parent::setUp();
        $this->fumocker = new Fumocker();
    }

    protected function tearDown(): void
    {
        $this->fumocker->unregister(); // unconditional clear
        parent::tearDown();
    }
}

Put this in tests/Support/ and extend it only from the tests that genuinely need it. Do not add it to tests/TestCase.php — that spreads the blast radius to your whole suite.

Scope the mock narrowly. The Scope concept exists because a global time() override poisoning the rest of the suite is the default failure mode. Register against the specific class (or class + method) under test, not globally, even when the broad version "works." Narrow scopes make failures local and make it obvious who owns the mock.

Use the callback as the observation point. The callback is where you observe arguments, which is the only way to assert "did it read the right config path":

$paths = [];
$this->fumocker->register('file_get_contents', function ($path) use (&$paths) {
    $paths[] = $path;
    return '{"ok":true}';
});

$this->assertContains(config_path('legacy.json'), $paths);

Mirror the real function's argument signature exactly (file_get_contents($path, $useIncludePath = false, $context = null)), otherwise Laravel/PHP will hand you arity errors that masquerade as mock failures.

Table-driven time tests instead of one test per date. Re-register between rows inside a single test:

foreach ([[1700000000, 'active'], [1730000000, 'expired']] as [$ts, $status]) {
    $this->fumocker->unregister('time');
    $this->fumocker->register('time', fn() => $ts);
    $this->assertSame($status, app(Subscription::class)->status());
}

This is cheaper than ten test methods and it documents the boundary conditions in one place.

Hybrid with PHPUnit/Laravel mocks. $this->mock(BillingGateway::class) for injected collaborators, Fumocker only for the function that can't be injected. Keep the Fumocker surface to one or two functions per test so the test stays readable. If a test needs four Fumocker registrations, that's a signal to refactor the class under test, not to write more mocks.

Treat it as a migration lever, not a destination. The pattern that pays off: freeze the function, write the failing test, extract a Clock / Randomizer / Filesystem contract, bind it in a service provider, delete the mock. Leave a // @todo extract Clock next to each registration. A growing count of those comments means the seam is quietly becoming permanent debt.

Prefer the Laravel-native fake first, and document why when you don't. Carbon::setTestNow(), Http::fake(), Storage::fake(), Config::set(), Queue::fake() are maintained, opcache-safe, and parallel-test-safe. Fumocker is the exception path for code that bypasses all of them — e.g. a vendored SDK calling curl_exec() directly, or a domain rule using rand().

Gotchas and Tips

  • It only affects code loaded through the scope. Fumocker doesn't patch the engine; it makes a target file see a differently-defined function. If the class under test was already autoloaded (very likely in Laravel — the container resolves it during bootstrap), or is served from opcache, your mock silently does nothing. Set opcache.enable_cli=0 in phpunit.xml/the CLI env and instantiate the class under test after registering.
  • Unqualified calls only. time() is interceptable; \time() is not. Code sitting in the global namespace is also a problem. This is the number-one cause of "the mock isn't firing" and it's identical to the constraint in php-mock.
  • Leaked mocks are silent and brutal. A time() you forgot to unregister makes an unrelated test fail an hour later on someone else's branch. Always unregister() in tearDown, and when debugging a flaky suite, grep -rn "register(" tests/ and verify every call is paired.
  • Double registration blows up with a redeclare/fatal error. Guard re-registration with unregister($fn) first (as in the table-driven pattern above) rather than assuming idempotency.
  • Parallel testing will bite you. Paratest / php artisan test --parallel forks processes, and a function mock that assumes a single process either doesn't apply or leaks oddly. Exclude Fumocker tests from parallel runs (@group legacy plus a serial CI job) until you've proven otherwise.
  • Pin an exact version and vendor it. 7 stars, 0 dependents, no vendor support, uncertain maintenance. "fp/fumocker": "dev-master#<sha>" or a committed fork. Budget for the day upstream breaks against a new PHP minor.
  • PHPUnit lifecycle matters. setUpBeforeClass runs before any mock is possible, and Laravel's RefreshDatabase / DatabaseTransactions hooks wrap around setUp/tearDown. Register inside setUp (or the test body) and clean in tearDown, not in class-level fixtures.
  • Debugging checklist, in order: (1) is opcache off for CLI? (2) is the call unqualified? (3) was the class instantiated after register()? (4) is the scope actually covering that file? (5) did a previous test leave a mock registered? Most "broken package" reports are one of those five.
  • Have an exit plan on day one. Write down the specific tests using it and the refactor that would remove each. If that list isn't short and shrinking within a sprint or two, you're maintaining a 7-star dependency for no reason — switch the same tests to php-mock/php-mock-phpunit (better maintained, same namespaced-function technique) or finish the dependency-injection refactor.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource