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

Async Test Utilities Laravel Package

wyrihaximus/async-test-utilities

Async test utilities for PHP that provide a TestCase with random namespaces and directories, fiber-based test execution with a configurable TimeOut attribute, and callable expectation helpers for ReactPHP async tests.

View on GitHub
Deep Wiki
Context7

Getting Started

Install as a dev dependency (the Composer name differs from the repo name and from the badge in the README):

composer require --dev wyrihaximus/async-test-utilities

It pulls in wyrihaximus/test-utilities, react/event-loop, react/promise, react/async and PHPUnit. PHP 8.4+ and PHPUnit ^12 are required — confirm that before you add it to an older Laravel 10/11 app.

The first place to look is WyriHaximus\AsyncTestUtilities\AsyncTestCase. Extend it instead of PHPUnit\Framework\TestCase, and every test method is transparently executed inside a Fiber with a 30-second guardrail. Your first use case is almost always "I have code that returns a React\PromiseInterface or uses React\Async\await(), and a normal PHPUnit test just returns immediately with an unresolved promise." Swap the base class and the test body can now await() like production code:

use React\Promise\Timer\sleep;
use WyriHaximus\AsyncTestUtilities\AsyncTestCase;
use function React\Async\await;

final class WorkerTest extends AsyncTestCase
{
    public function testJobIsConsumed(): void
    {
        await(sleep(0.05));
        self::assertTrue(true);
    }
}

Second thing to read is the TimeOut attribute (WyriHaximus\AsyncTestUtilities\TimeOut) — it is a class-level and method-level attribute, with method-level taking priority. Third are the callable expectations (expectCallableOnce(), expectCallableExactly(n)), which are the fastest way to assert that an event-loop callback fired the right number of times. Fourth are the random namespace/directory helpers inherited from test-utilities, useful when you test file- or cache-writing code without colliding across parallel test runs.

Implementation Patterns

Separate the async suite. Laravel's Illuminate\Foundation\Testing\TestCase and AsyncTestCase both extend PHPUnit\Framework\TestCase, so you cannot use both in one class. Create a second testsuite rather than trying to make them coexist:

<!-- phpunit.xml -->
<testsuites>
    <testsuite name="Feature"><directory>tests/Feature</directory></testsuite>
    <testsuite name="Async"><directory>tests/Async</directory></testsuite>
</testsuites>

Async tests live under tests/Async and never boot the full HTTP kernel. If you genuinely need a container-bound service, bootstrap the app manually in setUp() ($app = require __DIR__.'/../bootstrap/app.php'; $app->make(Kernel::class)->bootstrap();) and keep that bootstrap outside the fiber-heavy part of the test — you want container wiring done, then the loop work inside the test method.

Assert on the resolved value, not on the promise. The fiber wrapper waits for the method to return, but it does not automatically await an un-awaited promise. Always await() the thing you care about and assert on the resolved value:

$result = await($service->handle($command));
self::assertSame('done', $result);

Use callback expectations for fire-and-forget work. Anything you register with Loop::futureTick(), Loop::addTimer() or an emitter should be paired with an expectation, otherwise a silently-dropped event still yields a green test:

$listener = $this->expectCallableExactly(3);
Loop::futureTick($listener);
Loop::futureTick($listener);
Loop::futureTick($listener);

Tune timeouts per test class, not globally. Put a tight #[TimeOut(0.3)] on the class so hung loop tests fail in a fraction of a second, then loosen individual slow tests with a method-level #[TimeOut(5)]. This is the single biggest day-to-day win over the default 30s, which will silently make CI slow.

Test async adapters, not the whole framework. The sweet spot for a Laravel codebase is ReactPHP/AMPHP infrastructure you've wrapped: a socket server, a queue consumer loop, a cache or filesystem driver that returns promises, an Octane-adjacent worker. Instantiate the class directly, drive it with promises, await() the result, and use expectOutputString() when the component writes to stdout inside a loop callback.

Treat loop ordering as part of the assertion. Loop::futureTick() callbacks queued after your await() resolves may or may not have run. If order matters, assert it explicitly in the output string or by collecting into an array:

self::expectOutputString('ab');
Loop::futureTick(async(static function (): void { echo 'a'; }));
await(sleep(0.01));
echo 'b';

Gotchas and Tips

  • The v10 composer-plugin behaviour is intrusive. When wyrihaximus/makefiles is in require-dev, versions 10.0.0+ rewrite the post-install-cmd / post-update-cmd blocks of your composer.json to call make on-install-or-update || true, and will create those blocks if absent. In a Laravel app that already does @php artisan package:discover in those hooks this is a real conflict. Either don't install makefiles, or pin below 10.x, or audit composer.json after every composer update.
  • PHP native sleep() and other blocking calls defeat the timeout. The TimeOut attribute can only fire when control returns to the event loop. A test that blocks the fiber with sleep(60) or a synchronous HTTP client will blow past its timeout and stall the whole suite. Use React\Promise\Timer\sleep() instead.
  • The event loop is a global singleton. Timers and ticks registered in one test can fire during the next. If you see flaky assertions in the test after a loop test, the loop from the previous test is leaking. Drain it (Loop::run() at the end of risky tests) or make sure every timer you create is awaited/cancelled.
  • Unhandled promise rejections are swallowed or surface late. React logs unhandled rejections on destruction, which can appear as an error attributed to an unrelated later test. Attach a ->catch()/->then(null, ...) in the test, or assert the rejection explicitly.
  • expectOutputString counts output from fibers, so echo inside async() closures is captured — but output from the framework's own bootstrapping or deprecations (PHPUnit itself) will also be counted and break the equality assertion. Keep these tests free of app boot noise.
  • Attribute, not annotation. #[TimeOut(0.3)] — note the casing of the class (TimeOut), and that PHPUnit 12 has dropped the docblock equivalents. If you copy snippets from old blog posts using @timeout-style annotations, they are silently ignored.
  • Method-level timeout wins on precedence, so a slow method that inherits a strict class-level timeout looks like a random failure until you notice the method attribute is missing.
  • Overriding setUp()/tearDown(): call parent:: — the fiber wrapper and the random-directory cleanup depend on it. Skipping the parent call is the usual cause of "my temp directory survived the test run."
  • Pin the version and watch the release churn. With zero dependents and a single maintainer, essentially every release is a dependency bump (phpunit, makefiles, test-utilities) rather than an API change. Pin a minor range (^11.0) and update deliberately; a Renovate-style bump can jump you across a PHPUnit major.
  • Composer name confusion: the README's badges point at wyrihaximus/test-utilities while the install line says wyrihaximus/async-test-utilities. Use the latter; the namespace is WyriHaximus\AsyncTestUtilities\.
  • Extension points are thin. The intended customisation is subclassing AsyncTestCase to add your own helpers (a createService() factory, a database-free fixture loader) rather than configuring the package — there is essentially no config surface. That is fine, but don't hunt for a config/async-test-utilities.php.
  • Parallel test runners (ParaTest) work per-process, so each worker gets its own loop, but the loop is not reset between tests in a worker — the global-singleton caveat above is the one to watch.
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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource