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
Extend Base Test Case:
Replace your existing test case with WyriHaximus\AsyncTestUtilities\AsyncTestCase in your test files:
use WyriHaximus\AsyncTestUtilities\AsyncTestCase;
final class MyAsyncTest extends AsyncTestCase
{
// Tests here
}
First Use Case:
Test asynchronous behavior with Loop::futureTick and assertions:
public function testAsyncOutput(): void
{
self::expectOutputString('Hello World');
Loop::futureTick(static function (): void {
echo 'Hello ';
});
Loop::futureTick(static function (): void {
echo 'World';
});
}
$this->getRandomDirectory() for file system tests.$this->getRandomNamespace() for class/namespace tests.#[TimeOut(seconds)].Loop::futureTick to schedule async tasks and verify output/order.public function testAsyncOrder(): void
{
$output = [];
$this->expectCallableExactly(2, fn() => $output[] = 'done');
Loop::futureTick($this->expectCallableOnce());
Loop::futureTick($this->expectCallableOnce());
self::assertEquals(['done', 'done'], $output);
}
#[TimeOut(10)]
class MyTest extends AsyncTestCase { ... }
public function testFastOperation(): void
{
#[TimeOut(2)]
// Test logic here
}
$callable = $this->expectCallableExactly(3);
Loop::futureTick($callable);
Loop::futureTick($callable);
Loop::futureTick($callable);
Loop::futureTick($this->expectCallableOnce());
tests/TestCase.php:
use WyriHaximus\AsyncTestUtilities\AsyncTestCase;
abstract class TestCase extends AsyncTestCase { ... }
public function testQueueWorker(): void
{
$job = new ProcessPodcast();
$this->dispatch($job);
$this->expectCallableOnce(fn() => $this->assertDatabaseHas('jobs', ['id' => $job->getJobId()]));
}
public function testFileOperations(): void
{
$dir = $this->getRandomDirectory();
file_put_contents($dir.'/test.txt', 'Hello');
self::assertFileExists($dir.'/test.txt');
}
Artisan::call() inside Loop::futureTick.
public function testAsyncArtisan(): void
{
Loop::futureTick(fn() => Artisan::call('queue:work'));
$this->expectCallableOnce(fn() => $this->assertDatabaseHas('failed_jobs', [...]));
}
public function testAsyncEvent(): void
{
Event::fake();
Loop::futureTick(fn() => event(new PodcastProcessed()));
Event::assertDispatched(PodcastProcessed::class);
}
await() with React promises:
public function testAsyncPromise(): void
{
$promise = resolve('result');
self::assertEquals('result', await($promise));
}
Timeout Misconfiguration:
#[TimeOut] at method/class level or increase the default in the test case.TimeoutException.Callable Assertion Leaks:
expectCallable* callables may cause flaky tests.expectCallableOnce() for one-time operations.Event Loop Isolation:
File System Cleanup:
$this->getRandomDirectory() and let the base class handle cleanup.Loop::futureTick(fn() => error_log('Async call triggered'));
Loop::getCurrent() to verify the correct loop is active.$start = time();
Loop::futureTick(fn() => error_log("Execution time: " . (time() - $start)));
protected function setUp(): void
{
parent::setUp();
$this->timeout = 60; // 60 seconds
}
phpunit.xml includes:
<php>
<server name="APP_ENV" value="testing"/>
<server name="BCRYPT_ROUNDS" value="4"/>
</php>
AsyncTestCase to add domain-specific assertions:
protected function assertPodcastProcessed(string $podcastId): void
{
$this->expectCallableOnce(fn() => $this->assertDatabaseHas('podcasts', ['id' => $podcastId, 'processed' => true]));
}
trait AsyncQueueTests
{
protected function assertJobProcessed(string $jobId): void
{
$this->expectCallableOnce(fn() => $this->assertDatabaseMissing('failed_jobs', ['id' => $jobId]));
}
}
Loop::futureTick or React promises.public function testBatchAssertions(): void
{
$callable = $this->expectCallableExactly(5);
for ($i = 0; $i < 5; $i++) {
Loop::futureTick($callable);
}
}
How can I help you explore Laravel packages today?