Testing with the "in-memory" Transport

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

A few minutes ago, in the dev environment only, we overrode all our transports so that all messages were handled synchronously. We commented it out for now, but this is also something that you could choose to do in your test environment, so that when you run the tests, the messages are handled within the test.

This may or may not be what you want. On one hand, it means your functional test is testing more. On the other hand, a functional test should probably test that the endpoint works and the message is sent to the transport, but testing the handler itself should be done in a test specifically for that class.

That's what we're going to do now: figure out a way to not run the handlers synchronously but test that the message was sent to the transport. Sure, if we killed the worker, we could query the messenger_messages table, but that's a bit hacky - and only works if you're using the Doctrine transport. Fortunately, there's a more interesting option.

Start by copying config/packages/dev/messenger.yaml and pasting that into config/packages/test/. This gives us messenger configuration that will only be used in the test environment. Uncomment the code, and replace sync with in-memory. Do that for both of the transports.

framework:
messenger:
transports:
async: 'in-memory://'
async_priority_high: 'in-memory://'

The in-memory transport is really cool. In fact, let's look at it! I'll hit Shift+Shift in PhpStorm and search for InMemoryTransport to find it.

This... is basically a fake transport. When a message is sent to it, it doesn't handle it or send it anywhere, it stores it in a property. If you were to use this in a real project, the messages would then disappear at the end of the request.

But, this is super useful for testing. Let's try it. A second ago, each time we ran our test, our worker actually started processing those messages... which makes sense: we really were delivering them to the transport. Now, I'll clear the screen and then run:

php bin/phpunit

It still works... but now the worker does nothing: the message isn't really being sent to the transport anymore and it's lost at the end of our tests. But! From within the test, we can now fetch that transport and ask it how many messages were sent to it!

Fetching the Transport Service

Behind the scenes, every transport is actually a service in the container. Find your open terminal and run:

php bin/console debug:container async

There they are: messenger.transport.async and messenger.transport.async_priority_high. Copy the second service id.

We want to verify that the AddPonkaToImage message is sent to the transport, and we know that it's being routed to async_priority_high.

Back in the test, this is super cool: we can fetch the exact transport object that was just used from within the test by saying: $transport = self::$container->get() and then pasting the service id: messenger.transport.async_priority_high

... lines 1 - 8
class ImagePostControllerTest extends WebTestCase
{
public function testCreate()
{
... lines 13 - 25
$transport = self::$container->get('messenger.transport.async_priority_high');
... line 27
}
}

This self::$container property holds the container that was actually used during the test request and is designed so that we can fetch anything we want out of it.

Let's see what this looks like: dd($transport).

... lines 1 - 8
class ImagePostControllerTest extends WebTestCase
{
public function testCreate()
{
... lines 13 - 25
$transport = self::$container->get('messenger.transport.async_priority_high');
dd($transport);
}
}

Now jump back over to your terminal and run:

php bin/phpunit

Nice! This dumps the InMemoryTransport object and... the sent property indeed holds our one message object! All we need to do now is add an assertion for this.

Back in the test, I'm going to help out my editor by adding some inline docs to advertise that this is an InMemoryTransport. Below add $this->assertCount() to assert that we expect one message to be returned when we say $transport->... let's see... the method that you can call on a transport to get the sent, or "queued" messages is get().

... lines 1 - 6
use Symfony\Component\Messenger\Transport\InMemoryTransport;
class ImagePostControllerTest extends WebTestCase
{
public function testCreate()
{
... lines 13 - 24
/** @var InMemoryTransport $transport */
$transport = self::$container->get('messenger.transport.async_priority_high');
$this->assertCount(1, $transport->get());
}
}

Let's try it! Run:

php bin/phpunit

Got it! We're now guaranteeing that the message was sent but we've kept our tests faster and more directed by not trying to handle them synchronously. If we were using something like RabbitMQ, we also don't need to have that running whenever we execute our tests.

Next, let's talk deployment! How do we run our workers on production... and make sure they stay running?

Leave a comment!

This tutorial is built with Symfony 4.3, but will work well on Symfony 4.4 or 5.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "intervention/image": "^2.4", // 2.4.2
        "league/flysystem-bundle": "^1.0", // 1.1.0
        "sensio/framework-extra-bundle": "^5.3", // v5.3.1
        "symfony/console": "4.3.*", // v4.3.2
        "symfony/dotenv": "4.3.*", // v4.3.2
        "symfony/flex": "^1.1", // v1.4.4
        "symfony/framework-bundle": "4.3.*", // v4.3.2
        "symfony/messenger": "4.3.*", // v4.3.4
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/validator": "4.3.*", // v4.3.2
        "symfony/webpack-encore-bundle": "^1.5", // v1.6.2
        "symfony/yaml": "4.3.*" // v4.3.2
    },
    "require-dev": {
        "symfony/debug-pack": "^1.0", // v1.0.7
        "symfony/maker-bundle": "^1.0", // v1.12.0
        "symfony/test-pack": "^1.0" // v1.0.6
    }
}