feature #65900 [FrameworkBundle][Messenger] Add MessengerAssertionsTrait to test in-memory transports (nicolas-grekas)
This PR was merged into the 8.2 branch.
Discussion
----------
[FrameworkBundle][Messenger] Add MessengerAssertionsTrait to test in-memory transports
| Q | A
| ------------- | ---
| Branch? | 8.2
| Bug fix? | no
| New feature? | yes
| Deprecations? | no
| Issues | -
| License | MIT
This is a proposal, open for discussion. It started from Dariusz Gafka's article [Symfony Messenger vs Ecotone: The Real Difference](https://blog.ecotone.tech/ecotone-vs-symfony-messenger-the-real-difference/), which describes how Ecotone approaches this. What is proposed here is a free interpretation for Symfony, built on Messenger's own model rather than ported from Ecotone, so it departs from the article where the two models differ. The examples in this description are the article's.
Testing a flow whose second step is asynchronous currently means building a `Worker` by hand in the test, with an `EventDispatcher` and a `StopWorkerOnMessageLimitListener`, and handler failures never surface because the worker turns them into events. FrameworkBundle ships assertion traits for Mailer, Notifier, HttpClient and Console, but nothing for Messenger. This PR adds `MessengerAssertionsTrait` to `KernelTestCase`:
```php
$bus->dispatch(new LoanApplication('123', 25000));
$this->assertQueuedMessageCount(1, 'async');
$this->assertQueuedMessageCount(1, 'async', ScoreLoanApplication::class);
$this->consumeQueuedMessages('async'); // runs the queued messages through the real bus
// assert on what the handlers did
```
The transport must be configured as `in-memory://` in the test environment; the helpers fail with an explicit message otherwise.
## Public API
FrameworkBundle, `Symfony\Bundle\FrameworkBundle\Test\MessengerAssertionsTrait` (used by `KernelTestCase`, so also by `WebTestCase`):
- `assertQueuedMessageCount(int $count, string $transport, ?string $messageClass = null, string $message = '')`: number of envelopes queued on the transport, delayed ones included, optionally only the messages that are instances of the given class. A class name that does not exist is rejected, so a filter cannot silently match nothing after a rename.
- `getQueuedMessages(string $transport): Envelope[]`
- `getMessengerTransport(string $transport): InMemoryTransport`: the transport service, with a clear failure when the transport is not registered or not in-memory.
- `consumeQueuedMessages(string $transport, ?int $limit = null): int`: runs a real `Worker` on the transport with the application's routable bus and event dispatcher, stops when the transport is empty or after `$limit` messages, and returns the number of messages handled.
The application's worker listeners run as in production, with one difference that a test needs: a message sent for retry is consumed again without waiting for its delay, so the retries play out inside the call instead of leaving the message parked for `retry_strategy.delay` seconds. What that gives:
- a handler that throws once and succeeds on its retry lets the call return normally;
- a handler that keeps failing exhausts its retries in the same call, and the failure that exhausts them is rethrown, so a broken handler still fails the test;
- a failure that the strategy will replay is not rethrown, since it is not the outcome of the run;
- a message the application itself delayed is left in the transport until it is due, and the returned count shows that nothing was handled.
Messenger:
- `StopWorkerOnIdleListener`: stops the worker as soon as its receivers return no message (drain and exit).
- `InMemoryTransport` implements `ListableReceiverInterface` (`all()`, `find()`) and `MessageCountAwareInterface` (`getMessageCount()`), which also makes `messenger:show` and `messenger:stats` accept in-memory transports.
## Checks
- `./phpunit src/Symfony/Component/Messenger/Tests` and `./phpunit src/Symfony/Bundle/FrameworkBundle/Tests`: green (usual missing-server skips). The new functional test app (`Tests/Functional/app/Messenger`) covers counting, the class filter, the limit, the rethrow and both failure messages of `getMessengerTransport()`.
- Revert-verified, each behaviour against its own test: without the retry-aware reading, the two retry tests fail; without the gate on `willRetry()`, a handler that recovers on its retry still aborts the call; without the class check, the unknown-class test passes silently.
The functional test app keeps `retry_strategy.delay: 10000` on purpose: the retry tests prove that a ten second delay does not make the test wait.
## Documentation
- Configure `in-memory://` in `when@test`; the four methods and their semantics (delayed messages are counted and listed; envelopes, not messages, are returned).
- `consumeQueuedMessages()` runs the real bus and listeners; that retries are replayed without their delay while application delays are honoured; which failure is rethrown and which is not; and how to inspect the failure transport afterwards.
- `StopWorkerOnIdleListener` for hand-built workers.
Commits
-------
c7556a3b7fa [Messenger][FrameworkBundle] Add MessengerAssertionsTrait to test in-memory transports N
Nicolas Grekas committed
1f37304524cb8909b6682dde75b7bf909a3fc414