Poner en marcha un sistema de pruebas asesino
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeNuestra API es cada vez más compleja. Y hacer pruebas manualmente no es un buen plan a largo plazo. Así que vamos a instalar algunas herramientas para conseguir una configuración de pruebas asesina.
Instalar el paquete de pruebas
Primer paso: en tu terminal ejecuta:
composer require test
Este es un alias de flex para un paquete llamado symfony/test-pack. Recuerda: los paquetes son paquetes de acceso directo que en realidad instalan un montón de otros paquetes. Por ejemplo, cuando esto termine... y echemos un vistazo a composer.json, podrás ver abajo enrequire-dev que esto añadió el propio PHPUnit, así como algunas otras herramientas de Symfony para ayudar en las pruebas:
| { | |
| // ... lines 2 - 87 | |
| "require-dev": { | |
| // ... line 89 | |
| "phpunit/phpunit": "^9.5", | |
| "symfony/browser-kit": "6.2.*", | |
| "symfony/css-selector": "6.2.*", | |
| // ... lines 93 - 95 | |
| "symfony/phpunit-bridge": "^6.2", | |
| // ... lines 97 - 99 | |
| } | |
| } |
También ejecutó una receta que añadió varios archivos. Tenemos phpunit.xml.dist, un directorio tests/, .env.test para variables de entorno específicas de las pruebas e incluso un pequeño acceso directo ejecutable bin/phpunit que utilizaremos para ejecutar nuestras pruebas.
Biblioteca Hello browser
No es ninguna sorpresa, Symfony tiene herramientas para realizar pruebas y éstas pueden utilizarse para probar una API. Es más, API Platform incluso tiene sus propias herramientas construidas sobre ellas para que probar una API sea aún más fácil. Sin embargo, voy a ser testarudo y utilizar una herramienta totalmente diferente de la que me he enamorado.
Se llama Browser, y también está construida sobre las herramientas de prueba de Symfony: casi como una interfaz más bonita sobre esa sólida base. Es... superdivertido de usar. Browser nos proporciona una interfaz fluida que se puede utilizar para probar aplicaciones web, como la que ves aquí, o para probar APIs. También se puede utilizar para probar páginas que utilicen JavaScript.
Vamos a instalarlo. Copia la línea composer require, gira hacia atrás y ejecútalo:
composer require zenstruck/browser --dev
Mientras eso hace lo suyo, es opcional, pero hay una "extensión" que puedes añadir a phpunit.xml.dist. Añádela aquí abajo:
| // ... lines 1 - 3 | |
| <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
| xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" | |
| backupGlobals="false" | |
| colors="true" | |
| bootstrap="tests/bootstrap.php" | |
| convertDeprecationsToExceptions="false" | |
| > | |
| // ... lines 11 - 35 | |
| <extensions> | |
| <extension class="Zenstruck\Browser\Test\BrowserExtension" /> | |
| </extensions> | |
| // ... lines 39 - 45 | |
| </phpunit> |
En el futuro, si utilizas PHPUnit 10, es probable que esto se sustituya por alguna configuración de listener.
Esto añade algunas funciones extra al navegador. Por ejemplo, cuando falle una prueba, guardará automáticamente la última respuesta en un archivo. Pronto veremos esto. Y si utilizas pruebas con JavaScript, ¡hará capturas de pantalla de los fallos!
Crear nuestra primera prueba
Bien, ya estamos listos para nuestra primera prueba. En el directorio tests/, no importa cómo organices las cosas, pero yo voy a crear un directorio Functional/porque vamos a hacer pruebas funcionales a nuestra API. Sí, crearemos literalmente un cliente API, haremos peticiones GET o POST y luego afirmaremos que obtenemos de vuelta la salida correcta.
Crea una nueva clase llamada DragonTreasureResourceTest. Una prueba normal extiendeTestCase de PHPUnit. Pero haz que extienda KernelTestCase: una clase de Symfony que extiende TestCase... pero nos da acceso al motor de Symfony:
| // ... lines 1 - 2 | |
| namespace App\Tests\Functional; | |
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | |
| class DragonTreasureResourceTest extends KernelTestCase | |
| { | |
| } |
Empecemos probando la ruta de recolección GET para asegurarnos de que obtenemos los datos que esperamos. Para activar la biblioteca del navegador, en la parte superior, añade un trait con use HasBrowser:
| // ... lines 1 - 5 | |
| use Zenstruck\Browser\Test\HasBrowser; | |
| class DragonTreasureResourceTest extends KernelTestCase | |
| { | |
| use HasBrowser; | |
| // ... lines 11 - 18 | |
| } |
A continuación, añade un nuevo método de prueba: public functiontestGetCollectionOfTreasures() ... que devolverá void:
| // ... lines 1 - 7 | |
| class DragonTreasureResourceTest extends KernelTestCase | |
| { | |
| // ... lines 10 - 11 | |
| public function testGetCollectionOfTreasures(): void | |
| { | |
| // ... lines 14 - 17 | |
| } | |
| } |
Utilizar el navegador es sencillísimo gracias a ese trait: $this->browser(). Ahora podemos hacer peticiones GET, POST, PATCH o lo que queramos. Haz una petición GET a /api/treasures y luego, para ver qué aspecto tiene, utiliza esta ingeniosa función->dump():
| // ... lines 1 - 7 | |
| class DragonTreasureResourceTest extends KernelTestCase | |
| { | |
| // ... lines 10 - 11 | |
| public function testGetCollectionOfTreasures(): void | |
| { | |
| $this->browser() | |
| ->get('/api/treasures') | |
| ->dump() | |
| ; | |
| } | |
| } |
Ejecutando nuestras Pruebas a través del Binario symfony
¿A que mola? Veamos qué aspecto tiene. Para ejecutar nuestra prueba, podríamos ejecutar:
php ./vendor/bin/phpunit
Eso funciona perfectamente. Pero una de las recetas también añadió un archivo de acceso directo:
php bin/phpunit
Cuando lo ejecutamos, veamos. El dump() sí que funcionó: volcó la respuesta... que era una especie de error. Dice
SQLSTATE: falló la conexión al puerto 5432 del servidor.
No puede conectarse a nuestra base de datos. Nuestra base de datos se ejecuta a través de un contenedor Docker... y luego, como estamos utilizando el servidor web symfony, cuando utilizamos el sitio a través de un navegador, el servidor web symfony detecta el contenedor Docker y establece la variable de entorno DATABASE_URL por nosotros. Así es como nuestra API ha podido hablar con la base de datos Docker.
Cuando hemos ejecutado comandos que necesitan hablar con la base de datos, los hemos ejecutado como symfony console make:migration... porque cuando ejecutamos cosas a través desymfony, añade la variable de entorno DATABASE_URL... y luego ejecuta el comando.
Así que, cuando simplemente ejecutamos php bin/phpunit... falta el verdadero DATABASE_URL. Para solucionarlo, ejecuta:
symfony php bin/phpunit
Es lo mismo... excepto que deja que symfony añada la variable de entorno DATABASE_URL. Y ahora... ¡volvemos a ver el volcado! Desplázate hasta arriba. Mejor! Ahora el error dice
La base de datos
app_testno existe.
Base de datos específica de la prueba
Interesante. Para entender lo que está pasando, abre config/packages/doctrine.yaml. Desplázate hasta la sección when@test. Esto es genial: cuando estamos en el entorno test, hay un trozo de configuración llamado dbname_suffix. Gracias a esto, Doctrine tomará el nombre normal de nuestra base de datos y le añadirá _test:
| // ... lines 1 - 18 | |
| when@test: | |
| doctrine: | |
| dbal: | |
| # "TEST_TOKEN" is typically set by ParaTest | |
| dbname_suffix: '_test%env(default::TEST_TOKEN)%' | |
| // ... lines 24 - 44 |
La siguiente parte es específica de una biblioteca llamada ParaTest en la que puedes ejecutar pruebas en paralelo. Como no vamos a utilizar eso, es sólo una cadena vacía y no es algo de lo que debamos preocuparnos.
De todos modos, así es como acabamos con un _test al final del nombre de nuestra base de datos. Y eso es lo que queremos No queremos que nuestros entornos dev y test utilicen la misma base de datos, porque resulta molesto cuando se sobreescriben mutuamente.
Por cierto, si no estás utilizando la configuración binaria y Docker de symfony... y estás configurando tu base de datos manualmente, ten en cuenta que en el entorno testno se lee el archivo .env.local:
| # define your env variables for the test env here | |
| KERNEL_CLASS='App\Kernel' | |
| APP_SECRET='$ecretf0rt3st' | |
| SYMFONY_DEPRECATIONS_HELPER=999999 | |
| PANTHER_APP_ENV=panther | |
| PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots |
El entorno test es especial: se salta la lectura de .env.local y sólo lee .env.test. También puedes crear un .env.test.local para las variables de entorno que se leen en el entorno test pero que no se consignarán en tu repositorio.
El rasgo ResetDatabase
Vale, en el entorno test, nos falta la base de datos. Podríamos arreglarlo fácilmente ejecutando:
symfony console doctrine:database:create --env=test
Pero eso es demasiado trabajo. En lugar de eso, añade un rasgo más a nuestra clase de prueba:use ResetDatabase:
| // ... lines 1 - 6 | |
| use Zenstruck\Foundry\Test\ResetDatabase; | |
| class DragonTreasureResourceTest extends KernelTestCase | |
| { | |
| // ... line 11 | |
| use ResetDatabase; | |
| // ... lines 13 - 20 | |
| } |
Esto viene de Foundry: la biblioteca que hemos estado utilizando para crear fijaciones ficticias mediante las clases de fábrica. ResetDatabase es increíble. Se asegura automáticamente de que la base de datos se vacía antes de cada prueba. Así, si tienes dos pruebas, la segunda no se estropeará por culpa de algún dato que haya añadido la primera.
También va a crear la base de datos automáticamente por nosotros. Compruébalo. Ejecuta
symfony php bin/phpunit
de nuevo y comprueba el volcado. ¡Esa es nuestra respuesta! ¡Es nuestro hermoso JSON-LD! Todavía no tenemos ningún elemento en la colección, pero está funcionando.
Y fíjate en que, cuando hacemos esta petición, no estamos enviando una cabecera Accepten la petición. Recuerda que, cuando utilizamos la interfaz Swagger UI... en realidad sí envía una cabecera Accept que anuncia que queremos application/ld+json.
Podemos añadirlo a nuestra prueba si queremos. Pero si no pasamos nada, obtendremos JSON-LD de vuelta porque ése es el formato por defecto de nuestra API.
A continuación: vamos a terminar correctamente esta prueba, incluyendo la alimentación de la base de datos con datos y el aprendizaje de las aserciones de la API de Browser.
24 Comments
Hello,
I tried to do the API Part 3 directly but on 2nd chapter I got an error with tests running I couldn't handled. So I went back on tutorials to this one. I started to do it and the first time I ran phpunit, Bang, same error again. Here it is :
Can't find real help on google. The talk about a directory "test" in config/packages but I don't have one.
Any tip ?
Kind Regard,
Eric
Hey @Eric-J
Sorry for my late reply. It seems your tests are not running in the "test" environment. I'd need to look at your test code. What happens if you boot the kernel manually at the beginning of the test?
self::bootKernel();Hi @MolloKhan
The same happens to me
I'm using docker with symfony 7.0
.env.test
If i used
symfony console doctrine:database:create --env=testcreate the databaseTest code:
The config framework.yaml:
Work for me :
Hey Billy,
Thanks for sharing your solution with others!
Cheers!
Hello,
I got the same issue and eventually fixed it by running
export APP_ENV=testbefore running phpunit.Hope this helps
Hey Stoakes,
Thanks for sharing your solution with others! Yeah, you can export it with
exportcommand, or fo simplicity you can just run it asAPP_ENV=test symfony php bin/phpunit, i.e. add that ENV in front of the command.Or it should be enough to put that env var on the
phpunit.xml.distconfig file to do not forget to export that next time.Cheers!
Hey @JuanLuisGarciaBorrego
It's hard to tell what's wrong here. It could be due to Docker or Symfony 7, but let's try out a few things:
1) Try extending from
WebTestCasein your test class2) Re-install your vendors
3) Downgrade to Symfony 6.4
If I throw a custom Api Platform exception, It should return 400. But it returns a symfony error 500.
---- github/api-platform/core/issues/3239
After some digging, I found that api-platform only handles the exception if it's a route managed by the framework, in my case I was throwing the exception from a custom controller.
So, try adding either a $request->attributes->set( '_api_respond', true); at the beginning of your method (after injecting the Request), or a defaults={"_api_respond": true} in your @Route annotation (or defaults: ['_api_respond' => true] in your #[Route] attribute)
hey @Jay
Thanks for digging, I hope it will be helpful, however does it relates to current chapter or maybe some other?
Cheers
I'm not sure. It's defiantly related to the test system.
I just hope @weaverryan will mention this some where in the course.
so it's already mentioned here in comments, but if you will find a chapter where we can apply it then it will be a better win, until it will be mentioned here in comments ;)
Cheers!
Just in case anyone else stumbles over this: I had to name my local test env file
env.test.local(instead ofenv.local.test).You're right - I totally messed that up! Fixed the script at least in https://github.com/SymfonyCasts/api-platform3/commit/02f7a9ab05f872aa1d741335c8918f1f693bc4ef
Thanks!
Is there any reason to use
zenstruck/browserinstead ofsymfony/panther?(I've never used both of them but my first thought would have been to choose Panther to stay in the Symfony ecosystem)
Yo @Seb33300!
Fair question! I just REALLY like the user experience from anything built by
zenstruck. The fluid browser interface, for me, is more friendly than the native Symfony way of doing things. Also, the lead dev behind thezenstruckstuff is on the Symfony core team - so you're not straying too far from the ecosystem ;).Cheers!
is there any setup to IDE quick start buttons? My project runs with symfony cli, and i am not realy able to setup phpstorm to prefix phpunit with sysmfony.
Hey @Peter-P
I'm afraid I don't fully understand your problem. If you need PHPUnit autocompletion, you can install the "PHPUnit enhancement" plugin, and you can even make PhpStorm to run your tests (setting it up depends on your local environment)
Cheers!
Hey @MolloKhan
thanks for the quick reply. My question is there a way to use ide build in run test feature ctrl+shift-f10 on windows? I can setup phpstorm to run tests, but as i use symfony cli as a web server, phpunit cannot find out the port for database connection.
i hope it clarify my question.
thanks
Yes, you can configure PhpStorm to run your tests just like that, but the config depends entirely on your local environment. In my case, I use WSL Ubuntu under the hood, so I had to configure PhpStorm to work with WSL by setting up a WSL interpreter. I recommend you to take a look at JetBrains docs, they have very good docs and cover almost all of the cases
Cheers!
Hey, I've experienced some strange behaviors with my Database Tests. Next to my
tests/FunctionalI've created some Integration tests intests/Integrationand created a newDatabaseTestCasethat sole purpose is touse ResetDatabaseand a convenience functiongetEntityManager(). MyApiTestCase(and so all myFunctionalTests) extends fromDatabaseTestCase.Now, all my API tests uses ZenstruckFoundry to set up my test case, it works without any problem. When I try the same with an Integration test to test a common Service (that only extends
DatabaseTestCase) that tests myBudgetCategoryService::createBudgetCategoriesForEvent(Event $event), a method that creates new entities, adds them to the$eventand also persists them (no cascade persist), I getBut the event was created with
$event = EventFactory::createOne([...])->object();When I persist everything manually beforehand, it looks like it works, but in the end, I get lots of duplicate keys.
It seems like there are 2 different entity managers, but I am not sure and don't know why
Hey @Sebastian-K!
Sorry fro the slow reply! And yikes, this indeed sounds like a mystery/mess. Because of how odd this is, my first instinct is to look for some sort of misconfiguration. Do you have
use Factoriesat the top of your test class (or base class)? I had forgotten that once (actually, maybe in this tutorial) and thing worked fine for along time... until they didn't. I caught and fixed that in the code before recording episode 3 actually.Anyway, let's start by checking that part.
Cheers!
Yeah, that solved my problem. Thanks a lot!!! I've now added
use Factoriesto myDatabaseTestCasebut I don't understand why my 10 Api Tests works without a problem, they also use Zenstruck FactoriesI wondered about that exact thing. The system is complicated - so a bit of a mystery :)
"Houston: no signs of life"
Start the conversation!