Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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.

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

Nuestra 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:

102 lines composer.json
{
... 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:

47 lines phpunit.xml.dist
... 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_test no 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:

7 lines .env.test
# 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.

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.

Leave a comment!

5
Login or Register to join the conversation
Jay Avatar

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)

1 Reply
sadikoff Avatar sadikoff | SFCASTS | Jay | posted hace 2 meses | edited

hey @Jay

Thanks for digging, I hope it will be helpful, however does it relates to current chapter or maybe some other?

Cheers

Reply
Jay Avatar
Jay Avatar Jay | sadikoff | posted hace 2 meses | edited

I'm not sure. It's defiantly related to the test system.
I just hope @weaverryan will mention this some where in the course.

Reply

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!

Reply
Sebastian-K Avatar
Sebastian-K Avatar Sebastian-K | posted hace 2 días | edited

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

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice