PHPackages                             zenstruck/messenger-test - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. zenstruck/messenger-test

ActiveSymfony-bundle[Testing &amp; Quality](/categories/testing)

zenstruck/messenger-test
========================

Assertions and helpers for testing your symfony/messenger queues.

v1.14.0(3mo ago)2774.8M—0.6%13[10 issues](https://github.com/zenstruck/messenger-test/issues)10MITPHPPHP &gt;=8.1CI passing

Since Jan 12Pushed 3mo ago9 watchersCompare

[ Source](https://github.com/zenstruck/messenger-test)[ Packagist](https://packagist.org/packages/zenstruck/messenger-test)[ Docs](https://github.com/zenstruck/messenger-test)[ GitHub Sponsors](https://github.com/kbond)[ GitHub Sponsors](https://github.com/nikophil)[ RSS](/packages/zenstruck-messenger-test/feed)WikiDiscussions 1.x Synced 1mo ago

READMEChangelog (10)Dependencies (10)Versions (36)Used By (10)

zenstruck/messenger-test
========================

[](#zenstruckmessenger-test)

[![CI Status](https://github.com/zenstruck/messenger-test/workflows/CI/badge.svg)](https://github.com/zenstruck/messenger-test/actions?query=workflow%3ACI)[![Code Coverage](https://camo.githubusercontent.com/1c8d15c5b89e0cfdde8f358655c07e57925593c723cb3dbbe140b97876422bac/68747470733a2f2f636f6465636f762e696f2f67682f7a656e73747275636b2f6d657373656e6765722d746573742f6272616e63682f312e782f67726170682f62616467652e7376673f746f6b656e3d52374f48595947504b4d)](https://codecov.io/gh/zenstruck/messenger-test)

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

[](#installation)

1. Install the library:

    ```
    composer require --dev zenstruck/messenger-test
    ```
2. If not added automatically by Symfony Flex, add the bundle in `config/bundles.php`:

    ```
    Zenstruck\Messenger\Test\ZenstruckMessengerTestBundle::class => ['test' => true],
    ```
3. 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
---------

[](#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](#bus).

### Queue Assertions

[](#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

[](#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

[](#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): bool {
            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): bool => $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

[](#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

[](#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

[](#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
```

### Multiple Transports

[](#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();
    }
}
```

### Support of `DelayStamp`

[](#support-of-delaystamp)

Support of `DelayStamp` could be enabled per transport, within its dsn:

```
# config/packages/messenger.yaml

when@test:
    framework:
        messenger:
            transports:
                async: test://?support_delay_stamp=true
```

Note

Support of delay stamp was added in version 1.8.0.

#### Usage of a clock

[](#usage-of-a-clock)

Warning

Support of delay stamp needs an implementation of [PSR-20 Clock](https://www.php-fig.org/psr/psr-20/).

You can, for example use Symfony's clock component:

```
composer require symfony/clock
```

When using Symfony's clock component, the service will be automatically configured. Otherwise, you need to configure it manually:

```
# config/services.yaml
services:
    app.clock:
        class: Some\Clock\Implementation
    Psr\Clock\ClockInterface: '@app.clock'
```

#### Example of code supporting `DelayStamp`

[](#example-of-code-supporting-delaystamp)

Note

This example uses `symfony/clock` component, but you can use any other implementation of `Psr\Clock\ClockInterface`.

```
// Let's say somewhere in your app, you register some actions that should occur in the future:

$bus->dispatch(new Envelope(new TakeSomeAction1(), [DelayStamp::delayFor(new \DateInterval('P1D'))])); // will be handled in 1 day
$bus->dispatch(new Envelope(new TakeSomeAction2(), [DelayStamp::delayFor(new \DateInterval('P3D'))])); // will be handled in 3 days

// In your test, you can check that the action is not yet performed:

class TestDelayedActions extends KernelTestCase
{
    use InteractsWithMessenger;
    use ClockSensitiveTrait;

    public function testDelayedActions(): void
    {
        // 1. mock the clock, in order to perform sleeps
        $clock = self::mockTime();

        // 2. trigger the action that will dispatch the two messages

        // ...

        // 3. assert nothing happens yet
        $transport = $this->transport('async');

        $transport->process();
        $transport->queue()->assertCount(2);
        $transport->acknowledged()->assertCount(0);

        // 4. sleep, process queue, and assert some messages have been handled
        $clock->sleep(60 * 60 * 24); // wait one day
        $transport->process()->acknowledged()->assertContains(TakeSomeAction1::class);
        $this->asssertTakeSomeAction1IsHandled();

        // TakeSomeAction2 is still in the queue
        $transport->queue()->assertCount(1);

        $clock->sleep(60 * 60 * 24 * 2); // wait two other days
        $transport->process()->acknowledged()->assertContains(TakeSomeAction2::class);
        $this->asssertTakeSomeAction2IsHandled();
    }
}
```

#### `DelayStamp` and unblock mode

[](#delaystamp-and-unblock-mode)

"delayed" messages cannot be handled by the unblocking mechanism, `$transport->process()` must be called after a `sleep()` has been made.

### Enable Retries

[](#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
```

Note

When using retries along with `support_delay_stamp` you must mock the time to sleep between retries.

Bus
---

[](#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

[](#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

[](#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
---------------

[](#troubleshooting)

### Detached Doctrine Entities

[](#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`](https://github.com/symfony/symfony/blob/0e9cfc38e81464d9394ac6fa061e7962a6fe485d/src/Symfony/Bridge/Doctrine/Messenger/DoctrineClearEntityManagerWorkerSubscriber.php)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
```

###  Health Score

66

—

FairBetter than 99% of packages

Maintenance81

Actively maintained with recent releases

Popularity62

Solid adoption and visibility

Community34

Small or concentrated contributor base

Maturity73

Established project with proven stability

 Bus Factor1

Top contributor holds 79.2% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~54 days

Total

35

Last Release

90d ago

Major Versions

v0.6.0 → v1.0.02021-09-28

PHP version history (3 changes)v0.1.0PHP &gt;=7.4

v1.6.0PHP &gt;=8.0

v1.8.0PHP &gt;=8.1

### Community

Maintainers

![](https://www.gravatar.com/avatar/707369cc916e0ea1aacbf077dcba464f611cef879f024d8944311a54a15224b3?d=identicon)[kbond](/maintainers/kbond)

---

Top Contributors

[![kbond](https://avatars.githubusercontent.com/u/127811?v=4)](https://github.com/kbond "kbond (122 commits)")[![nikophil](https://avatars.githubusercontent.com/u/10139766?v=4)](https://github.com/nikophil "nikophil (17 commits)")[![flohw](https://avatars.githubusercontent.com/u/457663?v=4)](https://github.com/flohw "flohw (2 commits)")[![benito103e](https://avatars.githubusercontent.com/u/17587415?v=4)](https://github.com/benito103e "benito103e (1 commits)")[![carusogabriel](https://avatars.githubusercontent.com/u/16328050?v=4)](https://github.com/carusogabriel "carusogabriel (1 commits)")[![dazz](https://avatars.githubusercontent.com/u/182954?v=4)](https://github.com/dazz "dazz (1 commits)")[![HypeMC](https://avatars.githubusercontent.com/u/2445045?v=4)](https://github.com/HypeMC "HypeMC (1 commits)")[![jaugustin](https://avatars.githubusercontent.com/u/564420?v=4)](https://github.com/jaugustin "jaugustin (1 commits)")[![mickverm](https://avatars.githubusercontent.com/u/1834265?v=4)](https://github.com/mickverm "mickverm (1 commits)")[![norkunas](https://avatars.githubusercontent.com/u/2722872?v=4)](https://github.com/norkunas "norkunas (1 commits)")[![rodnaph](https://avatars.githubusercontent.com/u/447579?v=4)](https://github.com/rodnaph "rodnaph (1 commits)")[![romainallanot](https://avatars.githubusercontent.com/u/80783376?v=4)](https://github.com/romainallanot "romainallanot (1 commits)")[![aegypius](https://avatars.githubusercontent.com/u/25710?v=4)](https://github.com/aegypius "aegypius (1 commits)")[![tunterreitmeier](https://avatars.githubusercontent.com/u/26880534?v=4)](https://github.com/tunterreitmeier "tunterreitmeier (1 commits)")[![alli83](https://avatars.githubusercontent.com/u/67466942?v=4)](https://github.com/alli83 "alli83 (1 commits)")[![beau-ottens](https://avatars.githubusercontent.com/u/52446008?v=4)](https://github.com/beau-ottens "beau-ottens (1 commits)")

---

Tags

symfonytestdevqueueMessenger

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/zenstruck-messenger-test/health.svg)

```
[![Health](https://phpackages.com/badges/zenstruck-messenger-test/health.svg)](https://phpackages.com/packages/zenstruck-messenger-test)
```

###  Alternatives

[zenstruck/foundry

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

78611.9M97](/packages/zenstruck-foundry)[zenstruck/browser

A fluent interface for your Symfony functional tests.

2272.2M21](/packages/zenstruck-browser)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.3M152](/packages/sulu-sulu)[liip/test-fixtures-bundle

This bundles enables efficient loading of Doctrine fixtures in functional test-cases for Symfony applications

1798.3M42](/packages/liip-test-fixtures-bundle)[zenstruck/mailer-test

Alternative, opinionated helpers for testing emails sent with symfony/mailer.

46671.1k3](/packages/zenstruck-mailer-test)[zenstruck/console-test

Alternative, opinionated helper for testing Symfony console commands.

58635.0k20](/packages/zenstruck-console-test)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
