wyrihaximus/async-test-utilities
Async testing utilities for PHP/React: extend AsyncTestCase to run each PHPUnit test inside a Fiber with a default 30s timeout. Includes TimeOut attribute (class/method), plus helpers like random namespaces/directories and callable expectation utilities.
Installation:
composer require wyrihaximus/async-test-utilities --dev
Add to your composer.json under require-dev if not using globally.
Basic Test Case:
Extend WyriHaximus\AsyncTestUtilities\AsyncTestCase in your test file:
use WyriHaximus\AsyncTestUtilities\AsyncTestCase;
final class MyAsyncTest extends AsyncTestCase
{
public function testBasicAsync(): void
{
// Test logic here
}
}
First Use Case:
Test asynchronous behavior with Loop::futureTick and assertions:
public function testAsyncOutput(): void
{
self::expectOutputString('test');
Loop::futureTick(static function (): void {
echo 'test';
});
}
[TimeOut] attribute to control test execution time.expectCallableOnce() and expectCallableExactly() for verifying async callbacks.$this->getRandomDirectory() for file storage tests.Loop::futureTick() to schedule async tasks:
public function testAsyncTask(): void
{
$result = $this->expectCallableOnce();
Loop::futureTick(static function () use ($result): void {
$result('async-data');
});
}
[TimeOut] attribute (in seconds):
#[TimeOut(5)] // Class-level timeout (5s)
final class MyTest extends AsyncTestCase
{
#[TimeOut(1)] // Overrides class timeout for this method
public function testFastTimeout(): void
{
// ...
}
}
public function testCallbackInvocations(): void
{
$callback = $this->expectCallableExactly(2);
Loop::futureTick($callback);
Loop::futureTick($callback);
// Fails if called more/less than 2 times
}
$this->getRandomDirectory() for isolated file operations:
public function testFileOperations(): void
{
$dir = $this->getRandomDirectory();
file_put_contents($dir.'/test.txt', 'data');
$this->assertFileExists($dir.'/test.txt');
}
await() and async() for complex async flows:
public function testReactIntegration(): void
{
$promise = async(static function (): string {
return await(sleep(1)) . 'done';
});
$this->assertEquals('done', await($promise));
}
Service Container Integration: Bind async test utilities to Laravel’s container for reusable test setups:
// In a TestServiceProvider
$this->app->bind(AsyncTestCase::class, function () {
return new AsyncTestCase();
});
Database Transactions:
Combine with Laravel’s DatabaseTransactions trait for async DB tests:
use Illuminate\Foundation\Testing\DatabaseTransactions;
final class AsyncDatabaseTest extends AsyncTestCase
{
use DatabaseTransactions;
public function testAsyncDbOperation(): void
{
// ...
}
}
Event Testing:
Test async event listeners with expectCallableOnce():
public function testAsyncEventListener(): void
{
$listener = $this->expectCallableOnce();
event(new MyEvent());
// Assert listener was called
}
Queue Testing: Simulate async queue jobs:
public function testAsyncQueueJob(): void
{
$job = $this->expectCallableOnce();
dispatch(new MyJob($job));
}
Timeout Misconfiguration:
[TimeOut(30)] (default) and adjust per test.self::markTestSkipped() to debug flaky async tests.Callable Assertion Leaks:
afterTest() to reset:
protected function afterTest(): void
{
$this->callbacks = []; // Reset tracked callables
}
Fiber Context:
static vars) may leak between tests.setUp().ReactPHP Loop Conflicts:
AsyncTestCase).File Permissions:
$this->getRandomDirectory() may fail if temp dir lacks permissions.sys_get_temp_dir() for fallback:
$dir = $this->getRandomDirectory() ?: sys_get_temp_dir();
Log Async Output:
Use self::expectOutputString() to debug async echoes:
public function testAsyncEcho(): void
{
self::expectOutputString('debug:start');
Loop::futureTick(static function (): void {
echo 'debug:start';
});
}
Manual Timeout Adjustment: Override the default timeout dynamically:
public function testWithDynamicTimeout(): void
{
$this->setTimeout(10); // 10s timeout
// ...
}
Assertion Failures:
Loop::futureTick().self::assertTrue($this->callableWasInvoked()) for custom checks.Custom Assertions:
Extend AsyncTestCase to add domain-specific assertions:
final class ApiAsyncTest extends AsyncTestCase
{
protected function assertApiResponse(array $expected): void
{
// Custom logic
}
}
Mocking Async Services:
Replace Loop::futureTick() with a mock for isolated testing:
public function testMockedAsync(): void
{
$mock = $this->createMock(LoopInterface::class);
$mock->method('futureTick')->willReturnCallback($this->expectCallableOnce());
// Inject $mock into your async service
}
Parallel Test Execution:
Use AsyncTestCase alongside pestphp/pest-plugin-parallel for faster test suites:
// pest.php
uses(AsyncTestCase::class)->in('tests/Async');
Custom Timeouts: Override the default timeout logic in a subclass:
final class CustomTimeoutTest extends AsyncTestCase
{
protected function getDefaultTimeout(): float
{
return 60; // 1 minute
}
}
PHPUnit Version:
composer.json constraints).ReactPHP Dependencies:
react/event-loop and react/promise are installed (handled by Composer).Fiber Support:
Global State:
static or singleton services in tests; they may interfere with fiber isolation.How can I help you explore Laravel packages today?