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 Subscribe¿Cómo podemos escribir pruebas automatizadas para todo esto? Bueno... Tengo muchas respuestas para eso. En primer lugar, podrías hacer pruebas unitarias de tus clases de mensajes. Normalmente no lo hago... porque esas clases suelen ser muy sencillas... pero si tu clase es un poco más compleja o quieres ir a lo seguro, puedes hacer pruebas unitarias totalmente.
Más importantes son los manejadores de mensajes: definitivamente es una buena idea probarlos. Podrías escribir pruebas unitarias y simular las dependencias o escribir una prueba de integración... dependiendo de lo que sea más útil para lo que hace cada manejador.
La cuestión es: para las clases de mensajes y manejadores de mensajes... probarlas no tiene absolutamente nada que ver con Messenger o transportes o async o workers: son simplemente clases PHP bien escritas que podemos probar como cualquier otra cosa. Esa es realmente una de las cosas bonitas de Messenger: por encima de todo, sólo estás escribiendo código bonito.
Pero las pruebas funcionales son más interesantes. Por ejemplo, abresrc/Controller/ImagePostController.php
. El método create()
es la ruta de subida y hace un par de cosas: como guardar el ImagePost
en la base de datos y, lo más importante para nosotros, enviar el objeto AddPonkaToImage
.
Escribir una prueba funcional para este punto final es, en realidad, bastante sencillo, pero ¿qué pasaría si quisiéramos poder probar no sólo que este punto final "parece" haber funcionado, sino también que el objeto AddPonkaToImage
fue, de hecho, enviado al transporte? Al fin y al cabo, no podemos probar que Ponka se ha añadido realmente a la imagen porque, cuando se devuelve la respuesta, ¡todavía no ha ocurrido!
Primero vamos a poner en marcha la prueba funcional, antes de ponernos elegantes. Empieza por encontrar un terminal abierto y ejecutar:
composer require phpunit --dev
Eso instala el test-pack
de Symfony, que incluye el puente de PHPUnit - una especie de "envoltura" alrededor de PHPUnit que nos facilita la vida. Cuando termina, nos dice que escribamos nuestras pruebas dentro del directorio tests/
-una idea brillante- y que las ejecutemos ejecutando php bin/phpunit
. Ese pequeño archivo acaba de ser añadido por la receta y se encarga de todos los detalles de la ejecución de PHPUnit.
Bien, primer paso: crear la clase de prueba. Dentro de tests
, crea un nuevo directorio Controller/
y luego una nueva clase PHP: ImagePostControllerTest
. En lugar de hacer que ésta extienda la normal TestCase
de PHPUnit, extiende WebTestCase
, lo que nos dará los superpoderes de prueba funcional que merecemos... y necesitamos. La clase vive en FrameworkBundle pero... ¡ten cuidado porque hay (gasp) dos clases con este nombre! La que quieres vive en el espacio de nombres Test
. La que no quieres vive en el espacio de nombres Tests
... así que es súper confuso. Debería ser así. Si eliges la equivocada, borra la declaración use
e inténtalo de nuevo.
... lines 1 - 2 | |
namespace App\Tests\Controller; | |
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; | |
class ImagePostControllerTest extends WebTestCase | |
{ | |
... lines 9 - 12 | |
} |
Pero .... mientras escribía este tutorial y me enfadaba por esta parte confusa, creé una incidencia en el repositorio de Symfony. Y estoy encantado de que cuando grabé el audio, ¡la otra clase ya había sido renombrada! Gracias a janvt que se ha lanzado a ello. ¡Adelante con el código abierto!
De todos modos, como vamos a probar la ruta create()
, añadepublic function testCreate()
. Dentro, para asegurarme de que las cosas funcionan, voy a probar mi favorito $this->assertEquals(42, 42)
.
... lines 1 - 2 | |
namespace App\Tests\Controller; | |
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; | |
class ImagePostControllerTest extends WebTestCase | |
{ | |
public function testCreate() | |
{ | |
$this->assertEquals(42, 42); | |
} | |
} |
Fíjate en que no he obtenido ningún autocompletado en esto. Eso es porque el propio PHPUnit no se ha descargado todavía. Compruébalo: busca tu terminal y ejecuta las pruebas con:
php bin/phpunit
Este pequeño script utiliza Composer para descargar PHPUnit en un directorio separado en segundo plano, lo que es bueno porque significa que puedes obtener cualquier versión de PHPUnit, incluso si algunas de sus dependencias chocan con las de tu proyecto.
Una vez hecho esto... ¡ding! Nuestra única prueba está en verde. Y la próxima vez que ejecutemos
php bin/phpunit
salta directamente a las pruebas. Y ahora que PHPUnit está descargado, una vez que PhpStorm construya su caché, ese fondo amarillo en assertEquals()
desaparecerá.
Para probar la ruta en sí, primero necesitamos una imagen que podamos subir. Dentro del directorio tests/
, vamos a crear un directorio fixtures/
para contener esa imagen. Ahora copiaré una de las imágenes que he estado subiendo a este directorio y la llamaré ryan-fabien.jpg
.
Ahí lo tienes. La prueba en sí es bastante sencilla: crear un cliente con$client = static::createClient()
y un objeto UploadedFile
que representará el archivo que se está subiendo: $uploadedFile = new UploadedFile()
pasando la ruta del archivo como primer argumento - __DIR__.'/../fixtures/ryan-fabien.jpg
- y el nombre del archivo como segundo - ryan-fabien.jpg
.
... lines 1 - 5 | |
use Symfony\Component\HttpFoundation\File\UploadedFile; | |
class ImagePostControllerTest extends WebTestCase | |
{ | |
public function testCreate() | |
{ | |
$client = static::createClient(); | |
$uploadedFile = new UploadedFile( | |
__DIR__.'/../fixtures/ryan-fabien.jpg', | |
'ryan-fabien.jpg' | |
); | |
... lines 18 - 22 | |
} | |
} |
¿Por qué el segundo argumento, un poco "redundante"? Cuando subes un archivo en un navegador, éste envía dos informaciones: el contenido físico del archivo y el nombre del archivo en tu sistema de archivos.
Finalmente, podemos hacer la petición: $client->request()
. El primer argumento es el método... que es POST
, luego la URL - /api/images
- no necesitamos ningún parámetro GET o POST, pero sí necesitamos pasar un array de archivos.
... lines 1 - 5 | |
use Symfony\Component\HttpFoundation\File\UploadedFile; | |
class ImagePostControllerTest extends WebTestCase | |
{ | |
public function testCreate() | |
{ | |
$client = static::createClient(); | |
$uploadedFile = new UploadedFile( | |
__DIR__.'/../fixtures/ryan-fabien.jpg', | |
'ryan-fabien.jpg' | |
); | |
$client->request('POST', '/api/images', [], [ | |
... line 19 | |
]); | |
... lines 21 - 22 | |
} | |
} |
Si te fijas en ImagePostController
, esperamos que el nombre del archivo subido -que normalmente es el atributo name
del campo <input
- sea literalmente file
. No es el nombre más creativo... pero es sensato. Utiliza esa clave en nuestra prueba y ponla en el objeto $uploadedFile
.
... lines 1 - 5 | |
use Symfony\Component\HttpFoundation\File\UploadedFile; | |
class ImagePostControllerTest extends WebTestCase | |
{ | |
public function testCreate() | |
{ | |
$client = static::createClient(); | |
$uploadedFile = new UploadedFile( | |
__DIR__.'/../fixtures/ryan-fabien.jpg', | |
'ryan-fabien.jpg' | |
); | |
$client->request('POST', '/api/images', [], [ | |
'file' => $uploadedFile | |
]); | |
dd($client->getResponse()->getContent()); | |
} | |
} |
Y... ¡ya está! Para ver si ha funcionado, vamos add($client->getResponse()->getContent())
.
... lines 1 - 5 | |
use Symfony\Component\HttpFoundation\File\UploadedFile; | |
class ImagePostControllerTest extends WebTestCase | |
{ | |
public function testCreate() | |
{ | |
$client = static::createClient(); | |
$uploadedFile = new UploadedFile( | |
__DIR__.'/../fixtures/ryan-fabien.jpg', | |
'ryan-fabien.jpg' | |
); | |
$client->request('POST', '/api/images', [], [ | |
'file' => $uploadedFile | |
]); | |
dd($client->getResponse()->getContent()); | |
} | |
} |
¡Hora de probar! Busca tu terminal, limpia la pantalla, respira profundamente y...
php bin/phpunit
¡Ya está! Y obtenemos un nuevo identificador cada vez que lo ejecutamos. Los registros de ImagePost
se guardan en nuestra base de datos normal porque no me he tomado la molestia de crear una base de datos distinta para mi entorno test
. Eso es algo que normalmente me gusta hacer.
Elimina el dd()
: vamos a utilizar una aserción real: $this->assertResponseIsSuccessful()
.
... lines 1 - 7 | |
class ImagePostControllerTest extends WebTestCase | |
{ | |
public function testCreate() | |
... lines 11 - 21 | |
$this->assertResponseIsSuccessful(); | |
} | |
} |
Este bonito método se añadió en Symfony 4.3... y no es el único: ¡este nuevoWebTestAssertionsTrait
tiene un montón de nuevos y bonitos métodos para probar un montón de cosas!
Si nos detenemos ahora... esta es una bonita prueba y podrías estar perfectamente satisfecho con ella. Pero... hay una parte que no es ideal. Ahora mismo, cuando ejecutamos nuestra prueba, el mensaje AddPonkaToImage
se envía realmente a nuestro transporte... o al menos creemos que lo hace... no estamos verificando realmente que esto haya ocurrido... aunque podemos comprobarlo manualmente ahora mismo.
Para que esta prueba sea más útil, podemos hacer una de estas dos cosas. En primer lugar, podríamos anular los transportes para que sean síncronos en el entorno de prueba, como hicimos con dev
. Entonces, si el manejo del mensaje fallara, nuestra prueba fallaría.
O, en segundo lugar, podríamos al menos escribir algo de código aquí que demuestre que el mensaje se envió al menos al transporte. Ahora mismo, es posible que la ruta devuelva 200... pero algún error en nuestro código hizo que el mensaje nunca se enviara.
Añadamos esa comprobación a continuación, aprovechando un transporte especial "en memoria".
Hey Dilyan,
Thank you for this tip. Though, I'm not sure it works the exact way you described. Most probably the ".env.test" is created for you when you requires PHPUnit bridge with Composer (comes from the recipe), i.e. when executing "composer require phpunit".
Cheers!
Hey Ryan,
Thank for this nice tutorial.
I have error when I launch the test : FAILURES! Tests: 1, Assertions: 1, Failures: 1.
Like Jérôme, I have a big message of HTML code into terminal. Access denied for user root. How can I change this default user ?
Hey Stéphane,
First of all, you need to know the correct DB credentials. If those credentials are just defaults, like not secret ones - set them on DATABASE_URL env var in ".env" or ".env.test" respectively and commit them out. If your credentials are secret and you don't want to commit them - create ".env.local" or ".env.test.local" and set the them on DATABASE_URL there.
Cheers!
Hi, when I try dd($client->getResponses()->getContent())
, I get an error: <blockquote>Doctrine\DBAL\Exception\DriverException:\n
An exception occurred in driver: SQLSTATE[HY000] [2006] MySQL server has gone away\n</blockquote> I don't understand why as I'm just following the course and I didn't find a solution to fix the problem. Maybe I'm doing something wrong..? Can you help me please?
Hey Ajie62!
Hmm. that is a wild error! That usually happens if, for example, you have some long-running connection to the database... and the database eventually times out your connection. But in a functional test... that's very odd. Does it happen *every* time you run the test? And how long does it take for this error to show up - just a few seconds?
Cheers!
Hey, thanks for the answer. It happens every time I run the test and takes approximatively 3 seconds (more or less). I think it's weird because I wrote exactly what you wrote.
Hmm, yea, it's VERY odd. 3 seconds is not long enough for the database connection to "go away". Wait... let me think. Question:
1) Does dd($client->getResponses()->getContent())
actually cause the error? If you removed this line, do you not see the error?
2) Do you have any stack trace that goes with that error.
Something is fishy... :)
Yes, the dd
part causes the error. When I remove it, the problem disappears. There's a stack trace, but it's too big to show here. This stack trace includes the error message I mentioned earlier. Basically, it's a Symfony error page HTML that appears in the terminal.
Hmm, probably you need to tweak a bit your MySql config. I found this by googling your error
> The MySQL server has gone away (error 2006) has two main causes and solutions: Server timed out and closed the connection. To fix, check that wait_timeout mysql variable in your my.cnf configuration file is large enough. ... set max_allowed_packet = 128M , then restart your MySQL server: sudo /etc/init.d/mysql restart.
Could you give it a try to those possible fixes and let us know if it worked?
Cheers!
Already 3am here, I tried a lot of things, I set max_allowed_packet, added wait_timeout in my.cnf.. The last ten lines of the MySQL config on my Macbook Air are:
connect-timeout 0
max-allowed-packet 16777216
net-buffer-length 16384
select-limit 1000
max-join-size 1000000
show-warnings FALSE
plugin-dir (No default value)
default-auth (No default value)
binary-mode FALSE
connect-expired-password FALSE
I don't know what to do anymore.....
Hey Ajie62!
Can you take a screenshot of the error and stack trace in the terminal? Here's the part I'm not sure about yet: when you make a request, (A) does the error happen on that request... and then you print the error with dump() or (B) does the error NOT happen on the request, but when you dump the successful response, that causes something weird to happen and the database dies.
I think it's probably the first, but I don't know for sure. If it is the first, I'd be interested in finding out which code during the request caused the issue to happen. Another way to "sort of" check all of this would be to comment out the $messageBus->dispatch()
in the controller to see if that makes any difference.
I hope you're not losing too much sleep over this ;). These issues are the worst!
Cheers!
Hey, so I made the screenshots, but I don't know how I can give them to you. Please let me know! Thanks!
Hey Jerome,
It's pretty easy! You can upload to any image hosting you want (like Imgur service for example) and then just put links to the images in a new comment.
Cheers!
Ok, thank you victor! Does this work: https://imgur.com/a/SrD8HJR There are 2 pictures. If you need more, just tell me.
Hey Ajie62!
Thank you! I'm pretty sure that the database is "going away" inside the request itself - e.g. the POST request to the app_imagepost_create
route. So, let's do some debugging :). With weird things like this, I usually first try to eliminate as many lines of code as possible to see if we can identify one line that might be causing the problem. So, for example, in your controller, I would delete one line of code then try the test. If you get the same result, delete another line of code. Do this until the database stops "going away" or until you have a completely blank controller (well, you'll always need to return a response - so you could put a dummy return $this->json(['success' => true]);
at the bottom.
Let me know what you find out!
Cheers!
Ok, I just did what you suggested. I replaced the return
with the one you gave me, then deleted each line one by one. Finally, I think I know where the problem comes from... When I remove $entityManager->persist($imagePost);
and $entityManager->flush();
, the error disappears. When I put it back, the problem is here again. SO, there's a big chance it comes from the entityManager OR when I persist the $imagePost. But... Even though I know where the problem comes from, I don't know the origin... Why does it cause the request to fail?
[EDIT] : After I dumped the $entityManager
, I found out something interesting:
<blockquote>
-params: array:11 [
"driver" => "pdo_mysql"
"charset" => "utf8mb4"
"url" => "mysql://root:@127.0.0.1:3306/messenger_tutorial"
"host" => "127.0.0.1"
"port" => "3306"
"user" => "root"
"password" => ""
"driverOptions" => []
"serverVersion" => "5.7"
"defaultTableOptions" => array:2 [
"charset" => "utf8mb4"
"collate" => "utf8mb4_unicode_ci"
]
"dbname" => "messenger_tutorial"
]
</blockquote>
The URL parameter is incorrect. So I just wanted to let you know that first of all, I created a .env.local file, and the original .env file does still have the default values. The URL param of the entity manager is equal to the default value in .env, not my .env.local, and I can't manage to change this URL... Any idea?
Hey Ajie62!
Brilliant debugging! I think I know the cause now... though I'm still *quite* confused about why you're exactly getting a "MySQL has gone away"... I would expect something more like "Unable to connect" in this situation.
What's going on? By chance, I just talked about it on the API Platform security tutorial a few days ago: https://symfonycasts.com/sc...
So, yes, you're exactly right: your .env is being read and your .env.local is being completely ignored. This is a little "feature" that I like and hate at the same time, and argued against originally when it was added (though it IS useful half the time).
Let me know if this makes the difference :).
Cheers!
I tried the solution you gave me, but no success. I added the env variable DATABASE_URL in .env.test, but it looks like it's not being overriden. BUT, I managed to make it work (no problem anymore) by using the (basic) .env, which I didn't really want to do. SO, when I try to use .env.local
, .env.test
or .env.test.local
, nothing works. When I use .env
, it's okay... It's weird that I can't use another .env file...
I had same problem and I found that phpunit does not reads the .env files, instead it looks phpunit.xml in symfony's root dir. First copy
phpunit.xml.dist to phpunit.xml. after that in section <php> .. </php> add these lines with yout values
`
<php>
...
<server name="KERNEL_CLASS" value="App\Kernel" />
<server name="APP_SECRET" value="somesecret" />
<server name="DATABASE_URL" value="mysql://root:password@127.0.0.1:3306/the_spacebar" />
<server name="MESSENGER_TRANSPORT_DSN" value="doctrine://default" />
</php>
`
That works for me, I am using symfony 4.4.7
Hey Ajie62!
Nice job debugging! And yea... that is very weird :/. For reference, here is the line that skips the .env.local file in the test environment: https://github.com/symfony/symfony/blob/6dd9d24e5062544aac1367ee0de2801e860116ff/src/Symfony/Component/Dotenv/Dotenv.php#L94-L97 (or rather, this is the section that LOADS .env.local in all environments except for the $testEnvs
. And here are the lines that should ALWAYS load .env.test
and .env.test.local
https://github.com/symfony/symfony/blob/6dd9d24e5062544aac1367ee0de2801e860116ff/src/Symfony/Component/Dotenv/Dotenv.php#L103-L109 - AND, values from later files override values in earlier files.
Anyways, if you want to nerd out and do some debugging to figure out what was going wrong there, awesome. If not, then I'm glad we at least got it working :D.
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
}
}
If you don't have .env.test. The first time you run "php bin/phpunit" it will be created. Remember to open that file and add all of you credentials and secrets, otherwise the program/app/website will not work.