Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Probando con el transporte "en memoria

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

Hace unos minutos, sólo en el entorno dev, hemos anulado todos nuestros transportes para que todos los mensajes se manejen de forma sincrónica. Por ahora lo hemos comentado, pero esto es algo que también podrías hacer en tu entorno test, para que cuando ejecutes las pruebas, los mensajes se manejen dentro de la prueba.

Esto puede ser o no lo que quieres. Por un lado, significa que tu prueba funcional está probando más. Por otro lado, una prueba funcional probablemente debería probar que la ruta funciona y que el mensaje se envía al transporte, pero la prueba del propio manejador debería hacerse en una prueba específica para esa clase.

Eso es lo que vamos a hacer ahora: encontrar una forma de no ejecutar los manejadores de forma sincrónica, pero probar que el mensaje se ha enviado al transporte. Por supuesto, si matamos al trabajador, podemos consultar la tabla messenger_messages, pero eso es un poco complicado y sólo funciona si utilizas el transporte Doctrine. Afortunadamente, hay una opción más interesante.

Empieza copiando config/packages/dev/messenger.yaml y pegándolo enconfig/packages/test/. Esto nos da una configuración de Messenger que sólo se utilizará en el entorno test. Descomenta el código y sustituye sync porin-memory. Hazlo para los dos transportes.

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

El transporte in-memory es realmente genial. De hecho, ¡vamos a verlo! Voy a pulsarShift+Shift en PhpStorm y buscaré InMemoryTransport para encontrarlo.

Esto... es básicamente un transporte falso. Cuando se le envía un mensaje, no lo maneja ni lo envía a ningún sitio, lo almacena en una propiedad. Si utilizaras esto en un proyecto real, los mensajes desaparecerían al final de la petición.

Pero, esto es súper útil para hacer pruebas. Vamos a probarlo. Hace un segundo, cada vez que ejecutamos nuestra prueba, nuestro trabajador empezó a procesar esos mensajes... lo cual tiene sentido: realmente los estábamos entregando al transporte. Ahora, borraré la pantalla y luego ejecutaré:

php bin/phpunit

Sigue funcionando... pero ahora el trabajador no hace nada: el mensaje ya no se envía realmente al transporte y se pierde al final de nuestras pruebas. Pero, desde la prueba, ahora podemos recuperar ese transporte y preguntarle cuántos mensajes se le han enviado

Obtener el servicio de transporte

Entre bastidores, cada transporte es en realidad un servicio del contenedor. Busca tu terminal abierto y ejecuta:

php bin/console debug:container async

Ahí están: messenger.transport.async ymessenger.transport.async_priority_high. Copia el segundo id de servicio.

Queremos verificar que el mensaje AddPonkaToImage se envía al transporte, y sabemos que se dirige a async_priority_high.

De vuelta a la prueba, esto es superguay: podemos obtener el objeto de transporte exacto que se acaba de utilizar desde dentro de la prueba diciendo:$transport = self::$container->get() y pegando luego el id de serviciomessenger.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
}
}

Esta propiedad self::$container contiene el contenedor que se utilizó realmente durante la petición de la prueba y está diseñada para que podamos obtener lo que queramos de él.

Veamos qué aspecto tiene esto: 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);
}
}

Ahora vuelve a tu terminal y ejecuta:

php bin/phpunit

¡Bien! Esto vuelca el objeto InMemoryTransport y... ¡la propiedad sent contiene efectivamente nuestro objeto de mensaje! Todo lo que tenemos que hacer ahora es añadir una aserción para esto.

De vuelta a la prueba, voy a ayudar a mi editor añadiendo algunos documentos en línea para anunciar que esto es un InMemoryTransport. A continuación, añade $this->assertCount() para afirmar que esperamos que se devuelva un mensaje cuando digamos $transport->... veamos... el método al que puedes llamar en un transporte para obtener los mensajes enviados, o "en cola", es 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());
}
}

¡Vamos a probarlo! Ejecuta:

php bin/phpunit

¡Lo tengo! Ahora estamos garantizando que el mensaje se ha enviado, pero hemos mantenido nuestras pruebas más rápidas y dirigidas al no intentar manejarlas de forma sincrónica. Si utilizáramos algo como RabbitMQ, tampoco necesitaríamos tenerlo en marcha cada vez que ejecutamos nuestras pruebas.

A continuación, ¡hablemos del despliegue! ¿Cómo ejecutamos nuestros trabajadores en producción... y nos aseguramos de que siguen funcionando?

Leave a comment!

25
Login or Register to join the conversation
Braunstetter Avatar
Braunstetter Avatar Braunstetter | posted hace 4 meses

Hello dear Symfonycasts team,

I would like to use this in Behat.
I have also already injected the transport into my context and it works. But it is always empty, no matter what.

This will probably be because the is cleared on every other http request, right?

Is there a way to use this "in memory" transport in my Behat tests? Do you know something?

Kind regards
Michael

Reply

Hey @Braunstetter!

I have also already injected the transport into my context and it works. But it is always empty, no matter what.
This will probably be because the is cleared on every other http request, right?

I think you have identified the problem correctly :). When you use the "in memory" transport, it means that the messages in the queue are basically just stored in php. So they will exist until that request is over. In Behat, iirc, in practice, most of the ways that Mink makes requests involve making REAL requests to your app. So Mink makes a real HTTP request, you have an in-memory transport ON that request, then the request finishes and that is lost. Back in your context classes, the "in memory" transport you have there is (and never way) the same object in memory as the one that was used during the request :/. OR, perhaps Mink IS making "fake" requests through Symfony directly - the browser kit driver should be doing that. In that case, what you're attempting is possible, but my guess is that the "in memory" transport that's being used during the request is a different object than the one you're holding in the context.

ANYWAYS, if you ARE using browser-kit, I would recommend trying: https://github.com/zenstruck/messenger-test - you'll have to adjust the code a bit fore Behat. But this adds a "test" transport, where messages should be accessible even if the transport used during the request is a different instance than the one you fetch in your tests (because it stores the messages on a static property). If you're truly making different HTTP requests, then you'll need to change to use a real transport (e.g. doctrine) and then process those in your context after the request finishes. So yes, this stuff can be tricky!

Let me know if this helps... or didn't make any sense ;).

Cheers!

Reply
Braunstetter Avatar
Braunstetter Avatar Braunstetter | weaverryan | posted hace 3 meses | edited

Thank you Ryan.
I have now solved it as follows. when@test - I have now set the transport to sync and created an EventSubscriber that stores the message in the cache. I reset the whole thing before each scenario. Works great ;).

To be honest, I just copied it from the people at Sylius:
Sylius/Behat/Service/MessageSendCacher.php
Sylius/Behat/Context/Hook/MailerContext.php

Reply

Very cool solution! It's fund to see how you can hook in and, sort of, "steal" the messages for your own purposes ;). I love it!

1 Reply
Covi A. Avatar

when i using getConteainer->get() method it's giving me undefine getContainer method.
then i try to define getContainer() method. is it correct?
if yes then my question is what should need to return from this method?
or is it not correct can you please give me any suggestion what should i do? and how can solve this problem?
thank you.

Reply

Hey Monoranjan,

It looks like you have a misprint in the method name, you say "getConteainer" in the beginning and then "getContainer" later. Please, double check your method names in the code first.

I hope this help!

Cheers!

1 Reply
Covi A. Avatar

but the problem is still

Reply

Hey Monoranjan,

Could you give me a little bit more context? In what file you're calling that getContainer()? In what method? How exactly are you calling it, via "$this->getContainer()"? Too little information to help you further. :)

Cheers!

Reply
Covi A. Avatar
Covi A. Avatar Covi A. | Victor | posted hace 2 años | edited
tests/messenger.yaml
        transports:
            # https://symfony.com/doc/current/messenger.html#transport-configuration
            # async: '%env(MESSENGER_TRANSPORT_DSN)%'
            async: 'in-memory://'  

mytestclass


        public function testMyclass()
        {
            /** @var InMemoryTransport $transport */
            $transport = $this->getContainer()->get('messenger.transport.async');
            dd($transport);
        }

and result is

<blockquote>1) App\Tests\Controller\MessengerQueueTest::testMyclass
Error: Call to undefined method App\Tests\Controller\MessengerQueueTest::getContainer()
</blockquote>

Reply

Hey Monoranjan,

Thanks for providing more info! Ah, ok, what if you change that line to:


$transport = static::getContainer()->get('messenger.transport.async');

or maybe


$transport = self::$container->get('messenger.transport.async');

Does it work this way? Any new error message? :)

If not, please, tell me if you extends any class in that test class?

Cheers!

Reply
Covi A. Avatar
Covi A. Avatar Covi A. | Victor | posted hace 2 años | edited

Ah, sorry it's my fault. i was extending here TestCase But it should be needed WebTestCase.
Thanks for your help. it's working perfectly now with this syntax:


$transport = $this->getContainer()->get('messenger.transport.async');
Reply

Hey Monoranjan,

No problem! I happy it works for you now :) And thanks for sharing the final solution with others, it still might be helpful for others

Cheers!

Reply
Covi A. Avatar

sorry, i am very sorry for this fault.
but in the real code i don't do mistake like this. in the real code i write getContainer() every time.

Reply

Ah, ok, that was the first wild guess :)

Reply
Abdallah Avatar
Abdallah Avatar Abdallah | posted hace 2 años

is there solution to simulate retry with memory transport... ?

Reply

Hey Abdallah

I'm not sure but I don't think so because the Memory Transport it's kind of a dummy implementation of a transport. It's meant to be used on the dev or test environment

Cheers!

Reply
csc Avatar
csc Avatar csc | posted hace 2 años | edited

How can I run process queue (in test), manually ?

<br />/** @var InMemoryTransport $transport */<br />$transport = self::$container->get('messenger.transport.async_priority_high');<br />

I need check statuses before execute that queue

Thanks

Reply
Victor Avatar Victor | SFCASTS | csc | posted hace 2 años | edited

Hey Sebastian,

Good question! I believe you can achieve it with this simple code snippet:


        $number = 1; // How many messages to process

        $transport = $this->getContainer()->get('messenger.transport.async');
        $bus = $this->getContainer()->get(MessageBusInterface::class);
        $eventDispatcher = new EventDispatcher();
        $eventDispatcher->addSubscriber(new StopWorkerOnMessageLimitListener($number));

        $worker = new Worker([$transport], $bus, $eventDispatcher);
        $worker->run([
            'sleep' => 0,
        ]);

Just change your way to get Symfony services from the container, I see you do it via self::$container->get() instead. And don't forget to use your transport service name in that get().

I hope this helps!

Cheers!

2 Reply
Ed C. Avatar

Do you have an example of how to separately test the handler? I've got a test running as above for the message, but I also want to test that the handler is doing what I expect.

Reply

Hey Ed C.

A MessageHandler can be seen as any other service. You can test it by creating its related test class and just calling the invoke method like this:


$handler = new SomeMessageHandler($arg1, $arg2);
$r = $handler(); // Yes, handlers can return something if you need to.

// assert something

I hope it helps. Cheers!

Reply
Ed C. Avatar

Hi Diego,

That was it perfectly thanks!

Reply
Default user avatar

I have a problem using in-memory transport in my test environment. I use Behat scenarios with friends-of-behat/symfony-extension to inject symfony services in Context classes.
The problem is this: in the Context class I get a different instance of the InMemory transport. I know that the instance is different because I tried to inject the transport in both the Controller dispatching the message and in the Behat Context class. I dumped the two objects and are different.
Someone has an hint on why this is happening?
Thank you

Reply
Default user avatar
Default user avatar Marc | Matlar | posted hace 2 años | edited

Hi @Matlar and Matlar

I'm using friends-of-behat/Symfony-extension and in-memory transport too, and I'm facing the same problem. I think that the problem is that there are two different containers (one for the app and another for Mink), and they are not sharing their services. The state of the services is different, so we can't access the in-memory enqueued messages.

Do you know how to solve that without having to renounce to use Mink?

Thanks in advance :)

Reply

Hey Marc

think that the problem is that there are two different containers (one for the app and another for Mink), and they are not sharing their services. The state of the services is different, so we can't access the in-memory enqueued messages.

I'm not 100% sure about that. If you dump each container then you can confirm if they are the same object or not. In the case they're not, I think a good solution would be to use the doctrine transport and call the getMessageCount() method on it to assert if your message was dispatched.

Cheers!

1 Reply

Hey @Matlar

Did you configure your test enviroment to use the InMemory transport? In other words. Did you create a .env.local.test file with such Messenger transport configuration?

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

Este tutorial está construido con Symfony 4.3, pero funcionará bien en Symfony 4.4 o 5.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // v1.8.0
        "doctrine/doctrine-bundle": "^1.6.10", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // v2.0.0
        "doctrine/orm": "^2.5.11", // v2.6.3
        "intervention/image": "^2.4", // 2.4.2
        "league/flysystem-bundle": "^1.0", // 1.1.0
        "phpdocumentor/reflection-docblock": "^3.0|^4.0", // 4.3.1
        "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.9", // v1.18.7
        "symfony/framework-bundle": "4.3.*", // v4.3.2
        "symfony/messenger": "4.3.*", // v4.3.4
        "symfony/property-access": "4.3.*", // v4.3.2
        "symfony/property-info": "4.3.*", // v4.3.2
        "symfony/serializer": "4.3.*", // v4.3.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": {
        "easycorp/easy-log-handler": "^1.0.7", // v1.0.7
        "symfony/debug-bundle": "4.3.*", // v4.3.2
        "symfony/maker-bundle": "^1.0", // v1.12.0
        "symfony/monolog-bundle": "^3.0", // v3.4.0
        "symfony/stopwatch": "4.3.*", // v4.3.2
        "symfony/twig-bundle": "4.3.*", // v4.3.2
        "symfony/var-dumper": "4.3.*", // v4.3.2
        "symfony/web-profiler-bundle": "4.3.*" // v4.3.2
    }
}
userVoice