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.
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.
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!
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() | |
; | |
} | |
} |
¿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_test
no existe.
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 test
no 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.local.test
para las variables de entorno que se leen en el entorno test
pero que no se consignarán en tu repositorio.
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 Accept
en 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.
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!
Hey, I've experienced some strange behaviors with my Database Tests. Next to my tests/Functional
I've created some Integration tests in tests/Integration
and created a new DatabaseTestCase
that sole purpose is to use ResetDatabase
and a convenience function getEntityManager()
. My ApiTestCase
(and so all my FunctionalTests
) extends from DatabaseTestCase
.
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 my BudgetCategoryService::createBudgetCategoriesForEvent(Event $event)
, a method that creates new entities, adds them to the $event
and also persists them (no cascade persist), I get
* A new entity was found through the relationship 'App\Entity\BudgetCategory#event' that was not configured to
cascade persist operations for entity: App\Entity\Event@1673. To solve this issue: Either explicitly call
EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for
example @ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement
'App\Entity\Event#__toString()' to get a clue.
But 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
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^3.0", // v3.1.2
"doctrine/annotations": "^2.0", // 2.0.1
"doctrine/doctrine-bundle": "^2.8", // 2.8.3
"doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
"doctrine/orm": "^2.14", // 2.14.1
"nelmio/cors-bundle": "^2.2", // 2.2.0
"nesbot/carbon": "^2.64", // 2.66.0
"phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
"phpstan/phpdoc-parser": "^1.15", // 1.16.1
"symfony/asset": "6.2.*", // v6.2.5
"symfony/console": "6.2.*", // v6.2.5
"symfony/dotenv": "6.2.*", // v6.2.5
"symfony/expression-language": "6.2.*", // v6.2.5
"symfony/flex": "^2", // v2.2.4
"symfony/framework-bundle": "6.2.*", // v6.2.5
"symfony/property-access": "6.2.*", // v6.2.5
"symfony/property-info": "6.2.*", // v6.2.5
"symfony/runtime": "6.2.*", // v6.2.5
"symfony/security-bundle": "6.2.*", // v6.2.6
"symfony/serializer": "6.2.*", // v6.2.5
"symfony/twig-bundle": "6.2.*", // v6.2.5
"symfony/ux-react": "^2.6", // v2.7.1
"symfony/ux-vue": "^2.7", // v2.7.1
"symfony/validator": "6.2.*", // v6.2.5
"symfony/webpack-encore-bundle": "^1.16", // v1.16.1
"symfony/yaml": "6.2.*" // v6.2.5
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
"mtdowling/jmespath.php": "^2.6", // 2.6.1
"phpunit/phpunit": "^9.5", // 9.6.3
"symfony/browser-kit": "6.2.*", // v6.2.5
"symfony/css-selector": "6.2.*", // v6.2.5
"symfony/debug-bundle": "6.2.*", // v6.2.5
"symfony/maker-bundle": "^1.48", // v1.48.0
"symfony/monolog-bundle": "^3.0", // v3.8.0
"symfony/phpunit-bridge": "^6.2", // v6.2.5
"symfony/stopwatch": "6.2.*", // v6.2.5
"symfony/web-profiler-bundle": "6.2.*", // v6.2.5
"zenstruck/browser": "^1.2", // v1.2.0
"zenstruck/foundry": "^1.26" // v1.28.0
}
}
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)