zenstruck/messenger-test

repository·1.x·Indexed 42 days ago

https://github.com/zenstruck/messenger-test

A testing utility for Symfony Messenger that provides a TestTransport to intercept, inspect, and process queued messages. It includes the InteractsWithMessenger trait for KernelTestCase and WebTestCase, allowing developers to assert against message queues, verify serialization, and manually trigger message processing with filtering and exception handling.

Tokens
4.6K
Snippets
14
Records
15
Agent score
5%

What's inside zenstruck/messenger-test

  1. Overview of zenstruck/messenger-test

    1.x

    The zenstruck/messenger-test library provides assertions and helpers for testing symfony/messenger queues.

    It introduces a TestTransport that intercepts messages sent to it. Key features include:

    • Message Inspection: Assert against messages sent to the queue.
    • Serialization Check: Sent messages are automatically serialized and unserialized to ensure they can be correctly processed.
    • Queue Processing: The transport allows you to manually process messages that have been queued.
  2. Use Unblock Mode to handle messages immediately

    1.x

    By default, TestTransport intercepts messages and puts them in a queue for manual processing. In Unblock Mode, messages are handled immediately as they are sent.

    Methods:

    • $this->transport()->unblock(): Disables interception; messages are handled immediately.
    • $this->transport()->intercept(): Enables interception; messages are added to the queue.

    Configuration: You can enable this globally for a transport using the DSN option intercept=false.

    Note: If messages are already in the queue when you call unblock(), they will be processed.

    // Disable intercept so messages are handled immediately
    $this->transport()->unblock();
    
    // ... dispatch messages ...
    
    // Re-enable intercept for subsequent messages
    $this->transport()->intercept();
  3. Install zenstruck/messenger-test

    1.x

    Install the library via Composer for your development environment. If you are not using Symfony Flex, you must manually register the bundle in config/bundles.php for the test environment.

    composer require --dev zenstruck/messenger-test
    // config/bundles.php
    Zenstruck\Messenger\Test\ZenstruckMessengerTestBundle::class => ['test' => true],
  4. Test multiple transports

    1.x

    If your application uses multiple transports, set their DSNs to test:// in your test environment. You can then target specific transports in your tests by passing their name to the transport() method.

    Configuration:

    # config/packages/messenger.yaml
    when@test:
        framework:
            messenger:
                transports:
                    low: "test://"
                    high: "test://"

    Usage:

    $this->transport('high')->queue()->assertNotEmpty();
    $this->transport('low')->dispatched()->assertEmpty();
    // Access a specific transport by its name
    $this->transport('high')->queue();
    $this->transport('low')->dispatched();
  5. Interact with test transports using InteractsWithMessenger

    1.x

    To test Symfony Messenger transports, use the InteractsWithMessenger trait within your KernelTestCase or WebTestCase. This provides access to the transport() method, allowing you to assert against the queue and different message processing states like dispatched, acknowledged, and rejected.

    use Symfonyundleramework-bundle\Test\KernelTestCase;
    use Zenstruck\Messenger\Test\InteractsWithMessenger;
    
    class MyTest extends KernelTestCase
    {
        use InteractsWithMessenger;
    
        public function test_something(): void
        {
            // Access the transport via $this->transport()
        }
    }
  6. Assert message dispatching on the Bus

    1.x

    You can test if messages have been dispatched by a specific bus without needing to test the transport or the handling logic. To do this, use the InteractsWithMessenger trait in your KernelTestCase or WebTestCase. This allows you to use your custom transport while still asserting that messages are dispatched correctly.

    use Symfonyundleramework-bundle/test/kernel_test_case.php;
    use Zenstruck\Messenger\Test\InteractsWithMessenger;
    
    class MyTest extends KernelTestCase
    {
        use InteractsWithMessenger;
    
        public function test_something(): void
        {
            // ... code that triggers dispatching
    
            // Assert total count of dispatched messages
            $this->bus()->dispatched()->assertCount(2);
    
            // Assert specific message types and their occurrence counts
            $this->bus()->dispatched()->assertContains(MessageA::class, 1);
            $this->bus()->dispatched()->assertContains(MessageB::class, 1);
        }
    }
  7. Test delayed messages with DelayStamp and PSR-20 Clock

    1.x

    To test messages dispatched with a DelayStamp, you must:

    1. Enable support in the DSN: test://?support_delay_stamp=true.
    2. Provide a PSR-20 ClockInterface implementation (e.g., via symfony/clock).
    3. Use a clock-sensitive testing pattern to 'sleep' the time forward.

    Important: Delayed messages cannot be handled by the unblock() mechanism; you must call $transport->process() after advancing the clock.

    Example Workflow:

    1. Mock the clock.
    2. Dispatch messages with DelayStamp.
    3. Assert the messages are still in the queue().
    4. Advance the clock using $clock->sleep().
    5. Call $transport->process().
    6. Assert the messages are now in acknowledged().
    // 1. Mock the clock
    $clock = self::mockTime();
    
    // 2. Trigger dispatch
    $bus->dispatch(new Envelope(new MyMessage(), [DelayStamp::delayFor(new \DateInterval('P1D'))]));
    
    // 3. Assert nothing is acknowledged yet
    $transport->process();
    $transport->queue()->assertCount(1);
    
    // 4. Sleep and process
    $clock->sleep(60 * 60 * 24); // Advance 1 day
    $transport->process();
    
    // 5. Assert handled
    $transport->acknowledged()->assertContains(MyMessage::class);
  8. Handle exceptions in TestTransport

    1.x

    By default, TestTransport catches exceptions during message processing and adds the message to the rejected() collection.

    To test that an exception is actually thrown, you can:

    1. Use the DSN option catch_exceptions=false in your configuration.
    2. Use the transport methods $this->transport()->throwExceptions() and $this->transport()->catchExceptions() within your test.

    Configuration Example:

    # config/packages/messenger.yaml
    when@test:
        framework:
            messenger:
                transports:
                    async: "test://?catch_exceptions=false"
    // Manually toggle exception behavior in a test
    $this->transport()->throwExceptions();
    $this->transport()->process(1); // This will now throw the exception
    $this->transport()->catchExceptions();
  9. Configure the test transport in messenger.yaml

    1.x

    To use the library's testing capabilities, you must override your existing messenger transports with the test:// scheme within your test environment configuration. This allows the TestTransport to intercept messages sent to those transports.

    # config/packages/messenger.yaml
    
    # ...
    
    when@test:
        framework:
            messenger:
                transports:
                    async: test://
  10. Fix detached Doctrine entities during message processing

    1.x

    When processing messages in tests that interact with Doctrine entities, entities may become detached from the Object Manager. This is caused by the DoctrineClearEntityManagerWorkerSubscriber which clears the manager after a message is processed.

    To prevent this in your test environment, disable the service in your config/packages/messenger.yaml by overriding its class to stdClass.

    # config/packages/messenger.yaml
    
    when@test:
        services:
            # Disables the DoctrineClearEntityManagerWorkerSubscriber service
            doctrine.orm.messenger.event_subscriber.doctrine_clear_entity_manager:
                class: stdClass
  11. Use EnvelopeCollection assertions and helpers

    1.x

    The transport provides access to several EnvelopeCollection instances: queue(), dispatched(), acknowledged(), and rejected(). These collections are countable iterators with a specialized API.

    Collection Assertions:

    • assertEmpty(), assertNotEmpty(), assertCount(int $count), assertContains(string $class, ?int $count = null), assertNotContains(string $class).

    Helpers:

    • count(): Number of envelopes.
    • all(): Returns TestEnvelope[].
    • messages(): Returns object[] (unwrapped messages).
    • messages(string $class): Returns MyMessage[] (filtered by class).
    • first(): Returns the first TestEnvelope.
    • first(string $class): Returns the first TestEnvelope matching the class.
    • first(callable $filter): Returns the first TestEnvelope matching the callback.

    TestEnvelope Stamp Assertions: Since the collection returns TestEnvelope objects (decorators for Envelope), you can assert on stamps:

    • $envelope->assertHasStamp(string $stampClass)
    • $envelope->assertNotHasStamp(string $stampClass)
    $queue = $this->transport()->queue();
    
    // Collection assertions
    $queue->assertCount(3);
    $queue->assertContains(MyMessage::class);
    
    // Helpers
    $first = $queue->first(MyMessage::class);
    $messages = $queue->messages(MyMessage::class);
    
    // Stamp assertions on a TestEnvelope
    $queue->first()->assertHasStamp(DelayStamp::class);
    $queue->first()->assertNotHasStamp(DelayStamp::class);
    
    // Fluid assertions across different collections
    $this->transport()
        ->queue()->assertNotEmpty()->back()
        ->dispatched()->assertEmpty()->back()
        ->acknowledged()->assertEmpty()->back()
        ->rejected()->assertEmpty()->back();
  12. Assert messages on specific buses

    1.x

    If your application uses multiple message buses, you can target assertions to a specific bus by passing its name to the bus() method. This ensures you are verifying that the correct bus handled its intended messages.

    use Symfony\bundle\framework-bundle/test/kernel_test_case.php;
    use Zenstruck\Messenger\Test\InteractsWithMessenger;
    
    class MyTest extends KernelTestCase
    {
        use InteractsWithMessenger;
    
        public function test_something(): void
        {
            // Target a specific bus by name
            $this->bus('bus-a')->dispatched()->assertCount(1);
            $this->bus('bus-a')->dispatched()->assertContains(MessageA::class, 1);
    
            $this->bus('bus-b')->dispatched()->assertCount(1);
            $this->bus('bus-b')->dispatched()->assertContains(MessageB::class, 1);
        }
    }