wyrihaximus/async-test-utilities
Async test utilities for PHP/React tests. Extend AsyncTestCase to run each test inside a Fiber, get random namespaces/directories for filesystem tests, and control per-test or per-class timeouts via the TimeOut attribute (default 30s).
Installation:
composer require wyrihaximus/async-test-utilities --dev
Extend Base Test Case:
Replace your existing test case with WyriHaximus\AsyncTestUtilities\AsyncTestCase in your phpunit.xml or test class:
<testsuites>
<testsuite name="Application Test Suite">
<directory>./tests</directory>
<exclude>*/AsyncTestCase.php</exclude>
</testsuite>
<testsuite name="Async Test Suite">
<file>tests/AsyncTestCase.php</file>
</testsuite>
</testsuites>
First Use Case: Test async operations with a simple fiber-based test:
use WyriHaximus\AsyncTestUtilities\AsyncTestCase;
final class AsyncQueueTest extends AsyncTestCase
{
public function testAsyncQueueProcessing(): void
{
$this->expectCallableExactly(3);
// Simulate async processing
Loop::futureTick($this->expectCallableOnce());
Loop::futureTick($this->expectCallableOnce());
Loop::futureTick($this->expectCallableOnce());
}
}
expectCallableExactly(): Verify callable invocations in async contexts.expectCallableOnce(): Ensure a callable runs exactly once.TimeOut Attribute: Control test execution timeouts per test/method.getRandomDirectory() for isolated filesystem tests.Leverage fibers for non-blocking test execution:
public function testAsyncWorkflow(): void
{
$result = await(async(static function (): int {
return 42;
}));
self::assertSame(42, $result);
}
Use Loop::futureTick() for async test assertions:
public function testEventLoopBehavior(): void
{
$this->expectCallableOnce();
Loop::futureTick($this->expectCallableOnce());
Loop::futureTick(static function (): void {
// Simulate async work
});
}
Set granular timeouts with the TimeOut attribute:
#[TimeOut(5)] // Class-level timeout (5 seconds)
final class SlowAsyncTest extends AsyncTestCase
{
#[TimeOut(1)] // Overrides class timeout for this method
public function testFastOperation(): void
{
// ...
}
}
Generate isolated directories for temp file tests:
public function testFileOperations(): void
{
$tempDir = $this->getRandomDirectory();
file_put_contents($tempDir.'/test.txt', 'content');
self::assertFileExists($tempDir.'/test.txt');
}
Test async jobs with expectCallableExactly():
public function testJobDispatch(): void
{
$this->expectCallableExactly(2);
// Dispatch jobs
dispatch(new ProcessPodcast);
dispatch(new ProcessPodcast);
}
Test async command execution:
public function testAsyncArtisanCommand(): void
{
$this->expectCallableOnce();
Artisan::queue('podcast:process');
}
Use beginTransaction()/commit() with async operations:
public function testAsyncDatabaseOperations(): void
{
DB::beginTransaction();
try {
$this->expectCallableOnce();
Loop::futureTick($this->expectCallableOnce());
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
self::fail('Async operation failed');
}
}
Combine with spatie/async or laravel-horizon:
public function testHorizonJob(): void
{
$this->expectCallableOnce();
Horizon::dispatch(new ProcessPodcast);
}
Fiber Context Leaks
await promises can cause tests to hang.await() or use async():
$result = await(async(static fn() => sleep(1)));
Timeout Misconfiguration
#[TimeOut] at class/method level for precision.Callable Assertion Mismatches
expectCallableExactly() may fail if the callable isn't invoked in the expected fiber context.Loop::futureTick() or similar.Filesystem Cleanup
tearDown():
protected function tearDown(): void
{
if (isset($this->tempDir)) {
$this->removeDirectory($this->tempDir);
}
parent::tearDown();
}
Enable Fiber Debugging Add this to your test case for fiber stack traces:
public function setUp(): void
{
error_reporting(E_ALL);
ini_set('xdebug.show_exception_trace', '1');
parent::setUp();
}
Log Async Events
Use Loop::futureTick() with logging:
Loop::futureTick(static function (): void {
\Log::debug('Async event triggered');
});
Inspect Event Loop State Check pending futures:
public function testLoopState(): void
{
$pending = Loop::pendingFutures();
self::assertCount(0, $pending, 'Pending futures found');
}
Custom Assertions Extend the test case for domain-specific assertions:
final class ApiTestCase extends AsyncTestCase
{
protected function expectApiResponse(int $expectedStatus): void
{
// Custom logic
}
}
Mocking Async Components
Use Mockery or PHPUnit's mocks with async:
public function testMockedAsyncService(): void
{
$mock = $this->createMock(AsyncService::class);
$mock->method('process')->willReturn(resolve(42));
$result = await($mock->process());
self::assertSame(42, $result);
}
Integration with Laravel's Testing Helpers
Combine with Laravel\Testing\TestResponse:
public function testAsyncApiResponse(): void
{
$this->expectCallableOnce();
$response = $this->get('/api/endpoint');
$response->assertStatus(200);
}
PHPUnit Configuration
Ensure your phpunit.xml includes:
<php>
<server name="APP_ENV" value="testing"/>
<server name="BCRYPT_ROUNDS" value="4"/>
</php>
ReactPHP Dependencies
The package requires react/event-loop. Ensure compatibility:
composer require react/event-loop --dev
Fiber Support Requires PHP 8.1+. Verify your environment:
php -r "echo PHP_VERSION_ID >= 80100 ? 'Supported' : 'Unsupported';"
Parallel Test Execution
Use PHPUnit's --parallel flag for faster async test suites:
./vendor/bin/phpunit --parallel
Optimize Timeouts Set realistic timeouts to avoid flaky tests:
#[TimeOut(2)] // 2 seconds for fast operations
Avoid Blocking Calls
Prefer await() over synchronous calls in async tests:
// Bad: Blocks the fiber
$result = $syncMethod();
// Good: Non-blocking
$result = await(async(static fn() => $syncMethod()));
How can I help you explore Laravel packages today?