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 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.

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 test files:

    use WyriHaximus\AsyncTestUtilities\AsyncTestCase;
    
    final class MyAsyncTest extends AsyncTestCase
    {
        // Tests here
    }
    
  3. 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';
        });
    }
    

Key Starting Points

  • Random Directories: Use $this->getRandomDirectory() for file system tests.
  • Random Namespaces: Use $this->getRandomNamespace() for class/namespace tests.
  • Timeouts: Default is 30 seconds, override with #[TimeOut(seconds)].

Implementation Patterns

Core Workflows

1. Async Execution Testing

  • Pattern: Use 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);
}

2. Timeout Management

  • Class-level timeout (applies to all tests):
    #[TimeOut(10)]
    class MyTest extends AsyncTestCase { ... }
    
  • Method-level override (takes precedence):
    public function testFastOperation(): void
    {
        #[TimeOut(2)]
        // Test logic here
    }
    

3. Callable Assertions

  • Verify execution count:
    $callable = $this->expectCallableExactly(3);
    Loop::futureTick($callable);
    Loop::futureTick($callable);
    Loop::futureTick($callable);
    
  • Verify single execution:
    Loop::futureTick($this->expectCallableOnce());
    

4. Integration with Laravel

  • Service Container: Register the test case in tests/TestCase.php:
    use WyriHaximus\AsyncTestUtilities\AsyncTestCase;
    
    abstract class TestCase extends AsyncTestCase { ... }
    
  • Async Queues: Test queue workers by dispatching jobs and asserting completion:
    public function testQueueWorker(): void
    {
        $job = new ProcessPodcast();
        $this->dispatch($job);
    
        $this->expectCallableOnce(fn() => $this->assertDatabaseHas('jobs', ['id' => $job->getJobId()]));
    }
    

5. File System Tests

  • Temporary Directories:
    public function testFileOperations(): void
    {
        $dir = $this->getRandomDirectory();
        file_put_contents($dir.'/test.txt', 'Hello');
        self::assertFileExists($dir.'/test.txt');
    }
    

Integration Tips

Laravel-Specific

  • Artisan Commands: Test async commands with Artisan::call() inside Loop::futureTick.
    public function testAsyncArtisan(): void
    {
        Loop::futureTick(fn() => Artisan::call('queue:work'));
        $this->expectCallableOnce(fn() => $this->assertDatabaseHas('failed_jobs', [...]));
    }
    
  • Events: Assert event dispatching in async contexts:
    public function testAsyncEvent(): void
    {
        Event::fake();
        Loop::futureTick(fn() => event(new PodcastProcessed()));
        Event::assertDispatched(PodcastProcessed::class);
    }
    

ReactPHP Integration

  • Promises: Use await() with React promises:
    public function testAsyncPromise(): void
    {
        $promise = resolve('result');
        self::assertEquals('result', await($promise));
    }
    

Gotchas and Tips

Pitfalls

  1. Timeout Misconfiguration:

    • Issue: Tests hang if async operations exceed the timeout (default: 30s).
    • Fix: Use #[TimeOut] at method/class level or increase the default in the test case.
    • Debug: Check PHPUnit’s output for TimeoutException.
  2. Callable Assertion Leaks:

    • Issue: Unused expectCallable* callables may cause flaky tests.
    • Fix: Ensure all callables are invoked exactly as expected. Use expectCallableOnce() for one-time operations.
  3. Event Loop Isolation:

    • Issue: Tests may interfere if multiple event loops run concurrently.
    • Fix: Keep tests isolated; avoid global state in async tests.
  4. File System Cleanup:

    • Issue: Temporary directories/files may persist between tests.
    • Fix: Use $this->getRandomDirectory() and let the base class handle cleanup.

Debugging Tips

  • Log Async Calls:
    Loop::futureTick(fn() => error_log('Async call triggered'));
    
  • Inspect Event Loop: Use Loop::getCurrent() to verify the correct loop is active.
  • Timeout Debugging: Add a debug callable to measure execution time:
    $start = time();
    Loop::futureTick(fn() => error_log("Execution time: " . (time() - $start)));
    

Configuration Quirks

  1. Default Timeout:
    • Override in the test case constructor:
      protected function setUp(): void
      {
          parent::setUp();
          $this->timeout = 60; // 60 seconds
      }
      
  2. PHPUnit Integration:
    • Ensure phpunit.xml includes:
      <php>
          <server name="APP_ENV" value="testing"/>
          <server name="BCRYPT_ROUNDS" value="4"/>
      </php>
      

Extension Points

  1. Custom Assertions: Extend AsyncTestCase to add domain-specific assertions:
    protected function assertPodcastProcessed(string $podcastId): void
    {
        $this->expectCallableOnce(fn() => $this->assertDatabaseHas('podcasts', ['id' => $podcastId, 'processed' => true]));
    }
    
  2. Async Test Traits: Create reusable traits for common async patterns:
    trait AsyncQueueTests
    {
        protected function assertJobProcessed(string $jobId): void
        {
            $this->expectCallableOnce(fn() => $this->assertDatabaseMissing('failed_jobs', ['id' => $jobId]));
        }
    }
    

Performance Considerations

  • Avoid Blocking Calls: Ensure all async operations use Loop::futureTick or React promises.
  • Batch Assertions: Group related async assertions to reduce overhead:
    public function testBatchAssertions(): void
    {
        $callable = $this->expectCallableExactly(5);
        for ($i = 0; $i < 5; $i++) {
            Loop::futureTick($callable);
        }
    }
    
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.
hexters/coinpayment
rjcodes/rjcms
act-training/laravel-permissions-manager
alimarchal/laravel-chart-of-accounts
babenkoivan/elastic-scout-driver
mkwebdesign/filament-watchdog-v5
renatomarinho/laravel-page-speed
zedmagdy/filament-business-hours
renatovdemoura/blade-elements-ui
devgeek/beacon-admin
benjamin-rqt/data-watcher-bundle
atriumphp/atrium
sandermuller/package-boost-laravel
sandermuller/boost-skills
redaxo/core
yusufgenc/filament-api-forge
l3aro/rating-star-for-filament
leek/filament-subtenant-scope
anil/file-picker
broqit/fields-ai