Aprende Symfony2 (Parte 5): Tests

4 de octubre de 2014

Este es el quinto artículo de la serie para aprender sobre el framework Symfony2. En los anteriores artículos desarrollamos una aplicación sencilla que muestra información sobre empresas y que contiene los siguientes archivos:

.
├── app
│   ├── AppKernel.php
│   ├── cache
│   │   └── .gitkeep
│   ├── config
│   │   ├── config.yml
│   │   └── routing.yml
│   └── logs
│       └── .gitkeep
├── composer.json
├── composer.lock
├── src
│    └── AppBundle
│        ├── Controller
│        │   └── ApiController.php
│        └── AppBundle.php
├── .gitignore
└── web
    └── app.php

Ejecutar el comando composer install debería crear el directorio vendor/, que hemos ignorado en Git. Si lo necesitas, echa un vistazo al repositorio de código público en el que estamos desarrollando la aplicación. En este artículo crearemos tests funcionales utilizando PHPUnit.

Instalando PHPUnit

PHPUnit es un framework muy popular para crear tests. Su nombre engaña un poco, ya que no solo sirve para crear test unitarios, sino que puedes crear también tests funcionales, test tipo end to end, etc.

En primer lugar, instalemos PHPUnit en nuestro proyecto:

$ composer require --dev phpunit/phpunit

La opción --dev evitará que Composer instale PHPUnit cuando ejecutemos composer install --no-dev. Esto es especialmente útil en el entorno de producción, donde seguramente no querremos instalar PHPUnit.

Después necesitamos crear un archivo de configuración que le diga a PHPUnit que ejecute los tests que se encuentran en src/AppBundle/Tests, y que use Composer como autoloader:

<?xml version="1.0" encoding="UTF-8"?>
    <!-- File: app/phpunit.xml.dist -->

    <!-- http://phpunit.de/manual/current/en/appendixes.configuration.html -->
    <phpunit
        backupGlobals="false"
        colors="true"
        syntaxCheck="false"
        bootstrap="../vendor/autoload.php">

        <testsuites>
            <testsuite name="Functional Test Suite">
                <directory>../src/AppBundle/Tests</directory>
            </testsuite>
        </testsuites>

    </phpunit>

NOTA Una de las convenciones de Symfony dice que los tests deberían seguir la misma estructura de archivos y directorios que la aplicación. Por eso los tests deberían crearse en src/AppBundle/Tests. No es obligatorio, pero si quieres que otra gente encuentre las cosas donde esperan encontrarlas, es mejor que lo hagas así.

Este archivo lleva el sufijo .dist porque la idea es que cada desarrollador pueda sobreescribir la configuración creando un archivo app/phpunit.xml. Sólo el archivo común de tipo .dist debe ser publicado:

$ echo '/app/phpunit.xml' >> .gitignore
$ git add -A
$ git commit -m 'PHPUnit instalado'

Entornos de ejecución

Para nuestros tests funcionales, utilizaremos la clase WebTestCase, que instancia nuestro AppKernel con el entorno test. También usa el servicio test.client, que está desactivado por defecto.

Para activarlo, debemos modificar la configuración:

# app/config/config.yml
framework:
    secret: "El valor de esta opción debe ser una cadena aleatoria."
    router:
        resource: %kernel.root_dir%/config/routing.yml

    # test: ~

Normalmente no queremos que nuestra configuración sea la misma en nuestros tests y en nuestro servidor de producción. Para eso están los entornos. Coloquemos la configuración específica de nuestros tests en un archivo diferente:

# app/config/config_test.yml
imports:
    - { resource: config.yml }

framework:
    test: ~

NOTA El parámetro imports te permite incluir otros archivos de configuración. Así, puedes sobreescribir los parámetros incluídos, o añadir nuevos.

Deberíamos cambiar también el método registerContainerConfiguration de la clase AppKernel para que cargue la configuración de los tests dependiendo del entorno:

<?php
// app/AppKernel.php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        return array(
            new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
            new AppBundle\AppBundle(),
        );
    }

    public function registerContainerConfiguration(LoaderInterface $loader)
    {
        $file = 'config';
        if ('test' === $this->getEnvironment()) {
            $file .= '_test';
        }
        $loader->load(__DIR__."/config/$file.yml");
    }
}

Hagamos un commit al repositorio:

$ git add -A
$ git commit -m 'Añadida  la configuración de testing'

Tests funcionales

Nuestro test debe comprobar que la aplicación se comporta como esperamos. No comprobaremos que realmente ejecuta bien toda la lógica de negocio, por lo que comprobar el código de estado HTTP es más que suficiente.

Creemos el directorio:

$ mkdir -p src/AppBundle/Tests/Controller

NOTA En este caso, también por convención, se recomienda que la estructura de directorios de los tests sea la misma que la del bundle.

Y este es nuestro primer test funcional:

<?php
// src/AppBundle/Tests/Controller/ApiControllerTest.php
namespace AppBundle/Tests/Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class ApiControllerTest extends WebTestCase
{
    public function testInformacionSobreEmpresaExistente()
    {
        $method = 'POST';
        $uri = '/api/informacion';
        $parameters = array();
        $files = array();
        $server = array();
        $content = json_encode(array(
            'empresa' => 'ACME',
        ));

        $client = static::createClient();
        $client->request($method, $uri, $parameters, $files, $server, $content);
        $response = $client->getResponse();

        $this->assertTrue($response->isSuccessful());
    }
}

Para comprobar que pasamos el test, ejecuta el siguiente comando:

$ ./vendor/bin/phpunit -c app

Al instalar PHPUnit, Composer creó un archivo binario llamado phpunit en el directorio vendor/bin. La opción -c le dice a PHPUnit dónde está su archivo de configuración (en el caso de las aplicaciones Symfony, este archivo siempre se encuentra en app/).

El código de nuestro test es un poco largo por culpa de los parámetros. Para simplificarlo podemos utilizar los métodos auxiliares:

<?php
// src/AppBundle/Tests/Controller/ApiControllerTest.php

namespace AppBundle/Tests/Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class ApiControllerTest extends WebTestCase
{
    private function post($uri, array $data)
    {
        $content = json_encode($data);
        $client = static::createClient();
        $client->request('POST', $uri, array(), array(), array(), $content);

        return $client->getResponse();
    }

    public function testInformacionSobreEmpresaExistente()
    {
        $response = $this->post('/api/informacion', array('empresa' => 'ACME'));

        $this->assertTrue($response->isSuccessful());
    }
}

Ahora comprueba que el test sigue pasando correctamente:

$ ./vendor/bin/phpunit -c app

El método isSuccessful de Response sólo comprueba que el código de estado es 2xx. Aquí está el test para el caso de error:

<?php
// src/AppBundle/Tests/Controller/ApiControllerTest.php

namespace AppBundle/Tests/Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class ApiControllerTest extends WebTestCase
{
    private function post($uri, array $data)
    {
        $content = json_encode($data);
        $client = static::createClient();
        $client->request('POST', $uri, array(), array(), array(), $content);

        return $client->getResponse();
    }

    public function testInformacionSobreEmpresaExistente()
    {
        $response = $this->post('/api/informacion', array('empresa' => 'ACME'));

        $this->assertTrue($response->isSuccessful());
    }

    public function testInformacionSobreEmpresaInexistente()
    {
        $response = $this->post('/api/informacion', array('empresa' => 'NO_EXISTE'));

        $this->assertFalse($response->isSuccessful());
    }
}

Ahora ejecuta de nuevo los tests:

$ ./vendor/bin/phpunit -c app

NOTA A partir de ahora, deberías acostumrbarte a ejecutar los tests continuamente. Asegúrate de ejecutarlos siempre que termines un cambio, y de ejecutarlos de nuevo antes de añadir nada al repositorio.

Tests funcionales de la API Rest

En mi opinión, comprobar que el código de estado es 2xx y no comprobar el contenido de la respuesta es más que suficiente para un test funcional.

Al crear una API REST, puede ser útil testear con más precisión el código de estado. Nuestra aplicación es una API REST, así que hagámoslo:

<?php
// src/AppBundle/Tests/Controller/ApiControllerTest.php

namespace AppBundle/Tests/Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Response;

class ApiControllerTest extends WebTestCase
{
    private function post($uri, array $data)
    {
        $content = json_encode($data);
        $client = static::createClient();
        $client->request('POST', $uri, array(), array(), array(), $content);

        return $client->getResponse();
    }

    public function testInformacionSobreEmpresaExistente()
    {
        $response = $this->post('/api/informacion', array('empresa' => 'ACME'));

        $this->assertSame(Response::HTTP_OK , $response->getStatusCode());
    }

    public function testInformacionSobreEmpresaInexistente()
    {
        $response = $this->post('/api/informacion', array('empresa' => 'NO_EXISTE'));

        $this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY , $response->getStatusCode());
    }
}

Y ahora ejecuta de nuevo los tests:

$ ./vendor/bin/phpunit -c app

¡Verde! ¡Es suficiente recompensa como para hacer un commit al repositorio y dar la jornada por terminada!

$ git add -A
$ git commit -m 'Tests añadidos'

Conclusión

¡Ejecutar ./vendor/bin/phpunit -c app es más sencillo que tener que ejecutar manualmente HTTPie (como en el artículo anterior)!

Escribir tests funcionales es fácil y rápido, lo único que debes hacer es comprobar si el código de estado HTTP es correcto (y para una API REST, comprobar con precisión el código de estado HTTP).

El próximo artículo será un resumen de esta serie, ¡espero que te haya gustado!

Sobre el autor

Este artículo fue publicado originalmente por Loïc Chardonnet y ha sido traducido con permiso por Manuel Gómez.

Artículos de la serie Aprende Symfony