• Stars
    star
    222
  • Rank 179,123 (Top 4 %)
  • Language
    PHP
  • License
    MIT License
  • Created almost 4 years ago
  • Updated 4 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Assertions and helpers for testing your symfony/messenger queues.

zenstruck/messenger-test

CI Status Code Coverage

Assertions and helpers for testing your symfony/messenger queues.

This library provides a TestTransport that, by default, intercepts any messages sent to it. You can then inspect and assert against these messages. Sent messages are serialized and unserialized as an added check.

The transport also allows for processing these queued messages.

Installation

  1. Install the library:

    composer require --dev zenstruck/messenger-test
  2. Update config/packages/messenger.yaml and override your transport(s) in your test environment with test://:

    # config/packages/messenger.yaml
    
    # ...
    
    when@test:
        framework:
            messenger:
                transports:
                    async: test://

Transport

You can interact with the test transports in your tests by using the InteractsWithMessenger trait in your KernelTestCase/WebTestCase tests. You can assert the different steps of message processing by asserting on the queue and the different states of message processing like "acknowledged", "rejected" and so on.

Note: If you only need to know if a message has been dispatched you can make assertions on the bus itself.

Queue Assertions

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Zenstruck\Messenger\Test\InteractsWithMessenger;

class MyTest extends KernelTestCase // or WebTestCase
{
    use InteractsWithMessenger;

    public function test_something(): void
    {
        // ...some code that routes messages to your configured transport

        // assert against the queue
        $this->transport()->queue()->assertEmpty();
        $this->transport()->queue()->assertNotEmpty();
        $this->transport()->queue()->assertCount(3);
        $this->transport()->queue()->assertContains(MyMessage::class); // queue contains this message
        $this->transport()->queue()->assertContains(MyMessage::class, 3); // queue contains this message 3 times
        $this->transport()->queue()->assertContains(MyMessage::class, 0); // queue contains this message 0 times
        $this->transport()->queue()->assertNotContains(MyMessage::class); // queue not contains this message

        // access the queue data
        $this->transport()->queue(); // Envelope[]
        $this->transport()->queue()->messages(); // object[] the messages unwrapped from envelope
        $this->transport()->queue()->messages(MyMessage::class); // MyMessage[] just messages matching class
    }
}

Processing The Queue

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Zenstruck\Messenger\Test\InteractsWithMessenger;

class MyTest extends KernelTestCase // or WebTestCase
{
    use InteractsWithMessenger;

    public function test_something(): void
    {
        // ...some code that routes messages to your configured transport

        // let's assume 3 messages are on this queue
        $this->transport()->queue()->assertCount(3);

        $this->transport()->process(1); // process one message
        $this->transport()->processOrFail(1); // equivalent to above but fails if queue empty

        $this->transport()->queue()->assertCount(2); // queue now only has 2 items

        $this->transport()->process(); // process all messages on the queue
        $this->transport()->processOrFail(); // equivalent to above but fails if queue empty

        $this->transport()->queue()->assertEmpty(); // queue is now empty
    }
}

NOTE: Calling process() not only processes messages on the queue but any messages created during the handling of messages (all by default or up to $number).

Other Transport Assertions and Helpers

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\DelayStamp;
use Zenstruck\Messenger\Test\InteractsWithMessenger;
use Zenstruck\Messenger\Test\Transport\TestTransport;

class MyTest extends KernelTestCase // or WebTestCase
{
    use InteractsWithMessenger;

    public function test_something(): void
    {
        // manually send a message to your transport
        $this->transport()->send(new MyMessage());

        // send with stamps
        $this->transport()->send(Envelope::wrap(new MyMessage(), [new SomeStamp()]));

        // send "pre-encoded" message
        $this->transport()->send(['body' => '...']);

        $queue = $this->transport()->queue();
        $dispatched = $this->transport()->dispatched();
        $acknowledged = $this->transport()->acknowledged(); // messages successfully processed
        $rejected = $this->transport()->rejected(); // messages not successfully processed

        // The 4 above variables are all instances of Zenstruck\Messenger\Test\EnvelopeCollection
        // which is a countable iterator with the following api (using $queue for the example).
        // Methods that return Envelope(s) actually return TestEnvelope(s) which is an Envelope
        // decorator (all standard Envelope methods can be used) with some stamp-related assertions.

        // collection assertions
        $queue->assertEmpty();
        $queue->assertNotEmpty();
        $queue->assertCount(3);
        $queue->assertContains(MyMessage::class); // contains this message
        $queue->assertContains(MyMessage::class, 3); // contains this message 3 times
        $queue->assertNotContains(MyMessage::class); // not contains this message

        // helpers
        $queue->count(); // number of envelopes
        $queue->all(); // TestEnvelope[]
        $queue->messages(); // object[] the messages unwrapped from their envelope
        $queue->messages(MyMessage::class); // MyMessage[] just instances of the passed message class

        // get specific envelope
        $queue->first(); // TestEnvelope - first one on the collection
        $queue->first(MyMessage::class); // TestEnvelope - first where message class is MyMessage
        $queue->first(function(Envelope $e) {
            return $e->getMessage() instanceof MyMessage && $e->getMessage()->isSomething();
        }); // TestEnvelope - first that matches the filter callback

        // Equivalent to above - use the message class as the filter function typehint to
        // auto-filter to this message type.
        $queue->first(fn(MyMessage $m) => $m->isSomething()); // TestEnvelope

        // TestEnvelope stamp assertions
        $queue->first()->assertHasStamp(DelayStamp::class);
        $queue->first()->assertNotHasStamp(DelayStamp::class);

        // reset collected messages on the transport
        $this->transport()->reset();

        // reset collected messages for all transports
        TestTransport::resetAll();

        // fluid assertions on different EnvelopeCollections
        $this->transport()
            ->queue()
                ->assertNotEmpty()
                ->assertContains(MyMessage::class)
            ->back() // returns to the TestTransport
            ->dispatched()
                ->assertEmpty()
            ->back()
            ->acknowledged()
                ->assertEmpty()
            ->back()
            ->rejected()
                ->assertEmpty()
            ->back()
        ;
    }
}

Processing Exceptions

By default, when processing a message that fails, the TestTransport catches the exception and adds to the rejected list. You can change this behaviour:

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Zenstruck\Messenger\Test\InteractsWithMessenger;

class MyTest extends KernelTestCase // or WebTestCase
{
    use InteractsWithMessenger;

    public function test_something(): void
    {
        // ...some code that routes messages to your configured transport

        // disable exception catching
        $this->transport()->throwExceptions();

        // if processing fails, the exception will be thrown
        $this->transport()->process(1);

        // re-enable exception catching
        $this->transport()->catchExceptions();
    }
}

You can enable exception throwing for your transport(s) by default in the transport dsn:

# config/packages/messenger.yaml

# ...

when@test:
    framework:
        messenger:
            transports:
                async: test://?catch_exceptions=false

Unblock Mode

By default, messages sent to the TestTransport are intercepted and added to a queue, waiting to be processed manually. You can change this behaviour so messages are handled as they are sent:

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Zenstruck\Messenger\Test\InteractsWithMessenger;

class MyTest extends KernelTestCase // or WebTestCase
{
    use InteractsWithMessenger;

    public function test_something(): void
    {
        // disable intercept
        $this->transport()->unblock();

        // ...some code that routes messages to your configured transport
        // ...these messages are handled immediately

        // enable intercept
        $this->transport()->intercept();

        // ...some code that routes messages to your configured transport

        // if messages are on the queue when calling unblock(), they are processed
        $this->transport()->unblock();
    }
}

You can disable intercepting messages for your transport(s) by default in the transport dsn:

# config/packages/messenger.yaml

# ...

when@test:
    framework:
        messenger:
            transports:
                async: test://?intercept=false

Testing Serialization

By default, the TestTransport tests that messages can be serialized and deserialized. This behavior can be disabled with the transport dsn:

# config/packages/messenger.yaml

# ...

when@test:
    framework:
        messenger:
            transports:
                async: test://?test_serialization=false

Enable Retries

By default, the TestTransport does not retry failed messages (your retry settings are ignored). This behavior can be disabled with the transport dsn:

# config/packages/messenger.yaml

# ...

when@test:
    framework:
        messenger:
            transports:
                async: test://?disable_retries=false

Multiple Transports

If you have multiple transports you'd like to test, change all their dsn's to test:// in your test environment:

# config/packages/messenger.yaml

# ...

when@test:
    framework:
        messenger:
            transports:
                low: test://
                high: test://

In your tests, pass the name to the transport() method:

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Zenstruck\Messenger\Test\InteractsWithMessenger;

class MyTest extends KernelTestCase // or WebTestCase
{
    use InteractsWithMessenger;

    public function test_something(): void
    {
        $this->transport('high')->queue();
        $this->transport('low')->dispatched();
    }
}

Bus

In addition to transport testing you also can make assertions on the bus. You can test message handling by using the same InteractsWithMessenger trait in your KernelTestCase / WebTestCase tests. This is especially useful when you only need to test if a message has been dispatched by a specific bus but don't need to know how the handling has been made.

It allows you to use your custom transport while asserting your messages are still dispatched properly.

Single bus

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Zenstruck\Messenger\Test\InteractsWithMessenger;

class MyTest extends KernelTestCase
{
    use InteractsWithMessenger;

    public function test_something(): void
    {
        // ... some code that uses the bus

        // Let's assume two messages are processed
        $this->bus()->dispatched()->assertCount(2);

        $this->bus()->dispatched()->assertContains(MessageA::class, 1);
        $this->bus()->dispatched()->assertContains(MessageB::class, 1);
    }
}

Multiple buses

If you use multiple buses you can test that a specific bus has handled its own messages.

# config/packages/messenger.yaml

# ...

framework:
    messenger:
        default_bus: bus_c
        buses:
            bus_a: ~
            bus_b: ~
            bus_c: ~

In your tests, pass the name to the bus() method:

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Zenstruck\Messenger\Test\InteractsWithMessenger;

class MyTest extends KernelTestCase
{
    use InteractsWithMessenger;

    public function test_something(): void
    {
        // ... some code that use bus

        // Let's assume two messages are handled by two different buses
        $this->bus('bus-a')->dispatched()->assertCount(1);
        $this->bus('bus-b')->dispatched()->assertCount(1);
        $this->bus('bus-c')->dispatched()->assertCount(0);

        $this->bus('bus-a')->dispatched()->assertContains(MessageA::class, 1);
        $this->bus('bus-b')->dispatched()->assertContains(MessageB::class, 1);
    }
}

Troubleshooting

Detached Doctrine Entities

When processing messages in your tests that interact with Doctrine entities you may notice they become detached from the object manager after processing. This is because of DoctrineClearEntityManagerWorkerSubscriber which clears the object managers after a message is processed. Currently, the only way to disable this functionality is to disable the service in your test environment:

# config/packages/messenger.yaml

# ...

when@test:
    # ...

    services:
        # DoctrineClearEntityManagerWorkerSubscriber service
        doctrine.orm.messenger.event_subscriber.doctrine_clear_entity_manager:
            class: stdClass # effectively disables this service in your test env

More Repositories

1

foundry

A model factory library for creating expressive, auto-completable, on-demand dev/test fixtures with Symfony and Doctrine.
PHP
634
star
2

schedule-bundle

Schedule Cron jobs (commands/callbacks/bash scripts) within your Symfony application.
PHP
383
star
3

browser

A fluent interface for your Symfony functional tests.
PHP
185
star
4

messenger-monitor-bundle

Batteries included UI to monitor your Messenger workers, transports, schedules, and messages.
PHP
149
star
5

assert

Standalone, lightweight, framework agnostic, test assertion library.
PHP
56
star
6

console-extra

A modular set of features to reduce configuration boilerplate for your Symfony commands.
PHP
47
star
7

console-test

Alternative, opinionated helper for testing Symfony console commands.
PHP
34
star
8

callback

Callable wrapper to validate and inject arguments.
PHP
34
star
9

image

Image file wrapper with generic transformation support.
PHP
33
star
10

mailer-test

Alternative, opinionated helpers for testing emails sent with symfony/mailer.
PHP
33
star
11

redirect-bundle

Store redirects for your site and keeps statistics on redirects and 404 errors.
PHP
25
star
12

filesystem

Wrapper for league/flysystem with alternate API and added functionality.
PHP
17
star
13

uri

Object-oriented wrapper/manipulator for parse_url with additional features.
PHP
14
star
14

twig-service-bundle

Make functions, static methods, Symfony service methods available in your twig templates.
PHP
9
star
15

changelog

Generate pretty release changelogs using the commit log and Github API.
PHP
8
star
16

collection

Helpers for iterating/paginating/filtering collections (with Doctrine ORM/DBAL implementations and batch processing utilities).
PHP
8
star
17

signed-url-bundle

Helpers for signing and verifying urls with support for temporary and single-use urls.
PHP
6
star
18

class-metadata

Add human readable class aliases & metadata with efficient lookups.
PHP
5
star
19

dimension

Wrap quantity and unit of measure with conversions/humanizers.
PHP
4
star
20

redis

Lazy proxy for php-redis with DX helpers and utilities.
PHP
4
star
21

commonmark-extensions

A collection of CommonMark extensions.
PHP
4
star
22

phpmyadmin-server

Run phpMyAdmin in the background with a PHP webserver
PHP
3
star
23

dsn

DSN parsing library with support for complex expressions.
PHP
3
star
24

memoize

Helper trait to efficiently cache expensive methods in memory.
PHP
2
star
25

bytes

Parse, manipulate, humanize, and format bytes.
PHP
1
star
26

assert-html

Fluent html assertions plugin for zenstruck/assert.
PHP
1
star
27

stream

Object wrapper for PHP resources.
PHP
1
star
28

temp-file

Temporary file wrapper.
PHP
1
star
29

dom

DOM crawler with advanced selector API and assertions.
PHP
1
star