Weave Code
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/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).

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require wyrihaximus/async-test-utilities --dev
    
  2. 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>
    
  3. 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());
        }
    }
    

Key Features to Explore First

  • expectCallableExactly(): Verify callable invocations in async contexts.
  • expectCallableOnce(): Ensure a callable runs exactly once.
  • TimeOut Attribute: Control test execution timeouts per test/method.
  • Random Directories: Use getRandomDirectory() for isolated filesystem tests.

Implementation Patterns

Core Workflows

1. Async Test Execution

Leverage fibers for non-blocking test execution:

public function testAsyncWorkflow(): void
{
    $result = await(async(static function (): int {
        return 42;
    }));

    self::assertSame(42, $result);
}

2. Event Loop Integration

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
    });
}

3. Timeout Management

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
    {
        // ...
    }
}

4. Filesystem Testing

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');
}

Laravel-Specific Patterns

1. Queue Testing

Test async jobs with expectCallableExactly():

public function testJobDispatch(): void
{
    $this->expectCallableExactly(2);

    // Dispatch jobs
    dispatch(new ProcessPodcast);
    dispatch(new ProcessPodcast);
}

2. Artisan Command Async Testing

Test async command execution:

public function testAsyncArtisanCommand(): void
{
    $this->expectCallableOnce();

    Artisan::queue('podcast:process');
}

3. Database Transactions in Async Contexts

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');
    }
}

4. Integration with Laravel's Async Components

Combine with spatie/async or laravel-horizon:

public function testHorizonJob(): void
{
    $this->expectCallableOnce();

    Horizon::dispatch(new ProcessPodcast);
}

Gotchas and Tips

Common Pitfalls

  1. Fiber Context Leaks

    • Issue: Forgetting to await promises can cause tests to hang.
    • Fix: Always wrap async operations in await() or use async():
      $result = await(async(static fn() => sleep(1)));
      
  2. Timeout Misconfiguration

    • Issue: Default 30-second timeout may be too short/long for specific tests.
    • Fix: Use #[TimeOut] at class/method level for precision.
  3. Callable Assertion Mismatches

    • Issue: expectCallableExactly() may fail if the callable isn't invoked in the expected fiber context.
    • Fix: Ensure callables are invoked via Loop::futureTick() or similar.
  4. Filesystem Cleanup

    • Issue: Random directories aren't auto-cleaned.
    • Fix: Manually delete them in tearDown():
      protected function tearDown(): void
      {
          if (isset($this->tempDir)) {
              $this->removeDirectory($this->tempDir);
          }
          parent::tearDown();
      }
      

Debugging Tips

  1. 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();
    }
    
  2. Log Async Events Use Loop::futureTick() with logging:

    Loop::futureTick(static function (): void {
        \Log::debug('Async event triggered');
    });
    
  3. Inspect Event Loop State Check pending futures:

    public function testLoopState(): void
    {
        $pending = Loop::pendingFutures();
        self::assertCount(0, $pending, 'Pending futures found');
    }
    

Extension Points

  1. Custom Assertions Extend the test case for domain-specific assertions:

    final class ApiTestCase extends AsyncTestCase
    {
        protected function expectApiResponse(int $expectedStatus): void
        {
            // Custom logic
        }
    }
    
  2. 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);
    }
    
  3. 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);
    }
    

Configuration Quirks

  1. PHPUnit Configuration Ensure your phpunit.xml includes:

    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
    </php>
    
  2. ReactPHP Dependencies The package requires react/event-loop. Ensure compatibility:

    composer require react/event-loop --dev
    
  3. Fiber Support Requires PHP 8.1+. Verify your environment:

    php -r "echo PHP_VERSION_ID >= 80100 ? 'Supported' : 'Unsupported';"
    

Performance Tips

  1. Parallel Test Execution Use PHPUnit's --parallel flag for faster async test suites:

    ./vendor/bin/phpunit --parallel
    
  2. Optimize Timeouts Set realistic timeouts to avoid flaky tests:

    #[TimeOut(2)] // 2 seconds for fast operations
    
  3. 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()));
    
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.
davejamesmiller/laravel-breadcrumbs
artisanry/parsedown
christhompsontldr/phpsdk
enqueue/dsn
bunny/bunny
enqueue/test
enqueue/null
enqueue/amqp-tools
bower-asset/punycode
bower-asset/inputmask
bower-asset/jquery
bower-asset/yii2-pjax
laravel/nova
spatie/laravel-mailcoach
spatie/laravel-superseeder
laravel/liferaft
nst/json-test-suite
danielmiessler/sec-lists
jackalope/jackalope-transport
twbs/bootstrap4