If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeHace 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
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?
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!
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
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!
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.
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!
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!
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>
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!
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');
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!
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.
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!
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
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!
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.
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!
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
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 :)
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!
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!
// 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
}
}
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