Symfony: realizando tests con Panther

En este artículo quiero presentaros la librería Panther, la cual nos permite realizar tests end-to-end empleando navegadores reales.

Tal y como cuenta en su documentación, Panther aprovecha el protocolo WebDriver de cara a trabajar con navegadores nativos como Google Chrome o Firefox. Además, puesto que implementa las API’s de BrowserKit y DomCrawler de Symfony, su integración dentro de nuestro proyecto es muy sencilla ya que trabajaremos del mismo modo que hacemos cuando trabajamos creando tests funcionales.

Dicho esto, vamos a ver cómo trabajar con ella.

Pero antes… una breve una introducción.

Componentes para emular un navegador en Symfony

Cuando trabajamos con Symfony, tenemos 3 componentes para emular un navegador de modo que podamos escribir nuestros propios tests.

El primero de ellos es el BrowserKit Component, el cual simula el comportamiento de un navegador, permitiéndonos realizar peticiones, clickar enlaces o enviar formularios.

Junto a él, tenemos el DomCrawler Component, que simplifica la navegación a través del DOM de documentos HTML/XML y el CssSelector Component, que convierte expresiones CSS en selectores de XPath.

Si no estáis familiarizados con ellos, os recomiendo que os deis una vuelta por la documentación de Symfony ya que en ella se explica bastante bien la forma en que se emplean estos 3 componentes:

Creando tests funcionales en Symfony

Cuando queremos crear un test en Symfony, podemos hacerlo extendiendo de dos clases distintas:

  • KernelTestCase , la cual extiende de la clase TestCase , procedente de la librería PhpUnit y que se encarga de inicializar la aplicación.
  • WebTestCase , que extiende de la anterior y que emplea el componente BrowserKit para simular las peticiones, permitiéndonos obtener un objeto Response de cara a realizar las comprobaciones.

Dicho esto, gracias a la magia del MakerBundle, crear tests funcionales es una tarea bastante rápida.

Por ejemplo, imaginad que queremos implementar una serie de tests sobre un blog escrito en Symfony. Este blog posee las entidades habituales: Post , Category y Author así como una serie de controladores que nos permiten mostrar la lista de artículos ( BlogPostsController ) y un artículo individual ( BlogPostController ).

Así que, si queremos crear un test funcional para el controlador BlogPostController nos bastará con escribir:

bin/console make:functional-test \BlogPostsController

para generar el esqueleto de nuestro test funcional. Muy útil, ¿verdad? Hecho esto, bastará con completar nuestra test, el cual puede tener una forma similar a la siguiente:

Sin embargo, BrowserKit presenta alguna que otra limitación que es importante conocer si queremos realizar tests funcionales con este componente:

  • Tan sólo puede realizar peticiones internas a nuestra aplicación, de modo que si queremos realizar alguna petición externa necesitamos emplear otra librería.
  • No soporta Javascript, lo cual se convierte en un impedimento si nuestra aplicación emplea jQuery o cualquier otra librería / framework como React.
  • Y además puede que determinados problemas tan solo se presenten cuando empleamos un navegador real, como por ejemplo reglas CSS erróneas que nos impiden seleccionar un enlace o Javascript que no se ejecuta correctamente.

Es aquí cuando necesitamos otra alternativa si queremos que nuestros tests sean eficaces.

Características de Panther

Como os comentaba al principio, Panther es una librería que nos permite realizar tests funcionales empleando navegadores reales, lo cual nos permite trabajar con Javascript y CSS del mismo modo que haríamos si realizásemos pruebas manuales.

Además, implementa la API de BrowserKit, lo cual facilita mucho la transición de un componente a otro y nos provee de métodos muy útiles que nos permiten ejecutar código Javascript, esperar a que cargue cierta parte de la página o tomar una captura de pantalla.

Por otra parte, también destacan otra serie de características las cuales que nos vienen dadas de “gratis” cuando trabajamos con Panther:

  • Viene ya integrado con los binarios de ChromeDriver
  • Detecta automáticamente nuestra instalación de Chrome para trabajar con ella
  • Tiene una integración (aún experimental) con el driver Gecko (en el caso de que queramos simular Firefox)
  • Es compatible con servicios de testing remotos como BrowserStack
  • Panther sirve de forma transparente (y, por defecto, bajo el entorno de test) nuestro proyecto por medio del web-server de PHP incorporado heredando además las variables de entorno que hayamos establecido en el contexto actual.

Nuestro primer test con Panther

Puesto que Panther implementa la API de BrowserKit, crear tests con esta librería es muy sencillo. De hecho, partiendo del gist que os pegué unas líneas más atrás, podemos migrarlo muy rápidamente a Panther:

Por otra parte, cuando extendemos desde la clase PantherTestCase , además del propio cliente de Panther también podremos acceder a los siguientes métodos:

// Goutte: HTTP, web server pero sin JS
$client = static::createGoutteClient();
// WebTestCase: no HTTP, no JS, no web server
$client = static::createClient();

lo cual nos dará bastante flexibilidad en función de lo que necesitemos probar a la vez que mantenemos en ambos casos la misma API.

Características “molonas” de Panther

Además de permitirnos emplear un navegador real para realizar nuestros test, Panther nos da acceso a los siguientes métodos:

  • $client->waitFor('#an-element'); de modo que podamos esperar a que un determinado elemento del DOM sea cargado
  • $client->takeScreenshot('screenshot.png'); lo cual permite tomar capturas de pantalla de la situación actual del navegador
  • Y, aprovechando que el cliente devuelto por el método static::createPantherClient() implementa la interfaz WebDrive , podemos ejecutar código javascript de forma síncrona $client->executeScript('alert("Hola mundo")'); o asíncrona: $client->executeAsyncScript('alert("Hola mundo")');

Incrementando la velocidad de nuestros tests con Panther

Puesto que Panther trabaja con un navegador real, la ejecución de nuestros tests tardarán bastante más tiempo que cuando trabajamos con la clase WebTestCase . Esto es debido a que el webserver es finalizado en el método tearDownAfterClass y vuelto a inicializar cuando invocamos el método createClient . Sin embargo, tal y como se explica en la propia documentación podemos añadir lo siguiente en archivo phpunit.xml.dist :

<!-- phpunit.xml.dist -->
<extensions>
<extension class="Symfony\Component\Panther\ServerExtension" />
</extensions>

para que el webserver tan solo se finalice una vez que se hayan ejecutado todos los test.

Bola Extra: AliceBundle

Cuando queremos realizar tests funcionales que involucran bases de datos, se hace necesario tener un medio que nos permita generar datos de prueba. AliceBundle nos permite llevar a cabo estas cargas iniciales de una forma bastante sencilla:

Entre sus principales características podemos encontrar:

  • Integración completa con Faker, de cara a generar datos aleatorios.
  • Integración con la propia librería Alice, que añade azúcar sintáctico a Faker.
  • Integración completa con Symfony y con Flex
  • Soporte para Doctrine ORM y ODM.
  • E, inspirado por Lavarel, helpers para realizar pruebas en bases de datos.

Por ejemplo, gracias a este bundle podemos definir las fixtures de nuestros tests mediante un archivo yaml similar al siguiente:

# fixtures/blog_post.yaml
App\Entity\BlogPost:
  blog_post_{1..100}:

title: '<sentence()>'

body: '<paragraphs(10, true)>'

Algo que realmente simplifica enormemente la creación de datos.

Además, también nos permite recargar la base de datos en cada test mediante el uso del trait ReloadDatabaseTrait , de modo que cada test cuente con los datos iniciales que hayamos establecido:

...
use Hautelook\AliceBundle\PhpUnit\ReloadDatabaseTrait;
class BlogPostControllerTest extends PantherTestCase {
  use ReloadDatabaseTrait; 
  public function testShowPost() { 
  ...

Y hasta aquí el artículo sobre esta la librería Panther. Espero que os haya servido el artículo y me comentéis vuestras impresiones si os animáis a usarla.