Front & Back

Blog sobre el Desarrollo de aplicaciones web

Back end Cómo y por qué hacer tests funcionales en Symfony2

Como Robert C. Martin menciona en su famoso libro “Clean Code”, normalmente tendemos a tratar a los tests como “ciudadanos de segunda”; claro está que el elemento estrella es la funcionalidad, pero a veces las prisas por sacar nuestro software a producción nos hace obviar la importancia del testing. Deberíamos sin embargo tenerlo en cuenta como parte del código a entregar, y en la medida de lo posible considerar que hasta que no está bien realizado y distribuido, nuestro trabajo no ha finalizado. El test debe acompañar la evolución de nuestro software durante toda su vida útil y cambiar junto a él, debe estar tan cuidado y merecer tanta atención como el código que testea.

¿Pero por qué es tan importante?

Conforme crece un proyecto, el nivel de complejidad aumenta progresivamente, la cohesión del código suele ir pareja en muchos casos, y el control sobre la funcionalidad tiende a hacerse más difícil. Si hay varios desarrolladores implicados en el mismo, esta posibilidad aumenta, y lo hace aún más si existe rotación en la plantilla de desarrolladores. Suele ocurrir que el código acaba congelándose; nadie se atreve a tocar una sola línea de código, no vaya a ser que algo reviente por otro lado, y el desarrollo (que al principio era rápido) se detiene por completo.

Los test (si están bien enfocados) nos ayudan a evitar este tipo de catástrofes. Si cuando un desarrollador realiza un cambio puede lanzar el código de prueba, no tendrá miedo a refactorizar.

¿Funcionales o unitarios?

En mi último proyecto en Symfony2 me he decantado por centrarme en los test funcionales frente a los unitarios; las pruebas unitarias comprueban la integridad de un bloque de código (por ejemplo una clase) mientras que las funcionales testean la interacción y cohesión de las distintas partes que componen un programa, a través de una simulación de uso de cada una de sus funciones.

Creando los fixtures

Lo que vamos a hacer básicamente es crear una serie de datos ficticios (mocks) que utilizaremos en nuestros tests. En este caso se plantean test que se realizan sobre una aplicación que realiza operaciones sobre una base de datos mapeada con Doctrine. Las pruebas que hagamos sobre las funciones de nuestro software, comprobaran que los resultados que se obtengan son los esperados. Para crear estos datos utilizaremos DoctrineFixturesBundle que agregaremos a nuestro archivo app/AppKernel.php:

$bundles = array(
(…)
new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(),
(…)

y al composer.json:
"require": { 
(…)
"doctrine/doctrine-fixtures-bundle": "dev-master",
(…)

Despues del correspondiente “composer update” podremos crear nuestros fixtures (valores que se cargaran en base de datos) en los directorios DataFixtures/ORM de los bundles que queramos testear. Un ejemplo de fixture sería el siguiente:

namespace JVC\MainBundle\DataFixtures\ORM;

use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;

use JVC\MainBundle\Entity\MailInfo;

class LoadMailInfoData extends AbstractFixture implements OrderedFixtureInterface, FixtureInterface, ContainerAwareInterface
{
/**
* @var ContainerInterface
*/
private $container;

public function setContainer(ContainerInterface $container = null) {
$this->container = $container;
}
public function load(ObjectManager $manager) {
if($this->container->get('kernel')->getEnvironment() == 'test') //Estos valores sólo se volcarán en el entorno de test
{
$mailInfo = new MailInfo();
$mailInfo ->setField(‘MAIL_TEST’);
$mailInfo ->setValue('VALUE');
$this->persist($mailInfo, $manager);
}
}
public function getOrder() {
return 1;
}
private function persist($object, $manager) {
$manager->persist($object);
return $manager->flush();
}
}

Configurando el entorno de pruebas

Como vemos en el bloque de código anterior, sólo queremos que estos datos estén disponibles cuando realicemos los tests; para ello deberemos configurar un nuevo entorno de testing. Crearemos los archivos:

- app/config/config_test.yml – que contendrá la configuración específica de nuestro nuevo entorno.
- app/config/parameters_test.yml.dist – al que se hará referencia desde config_test.yml, que contendrá los parámetros de configuración de la base de datos de testing y que renombraremos cuando despleguemos nuestro proyecto en desarrollo.

Cargando los registros de prueba en base de datos

Una vez habilitemos la base de datos de pruebas y hayamos asignado sus parámetros en parameters_test.yml, crearemos el esquema y volcaremos los fixtures con los comandos:

#Es importante la opción “—env test” porque hará que nuestros datos vayan a BD de testing
$ php app/console doctrine:schema:update –env test --force
$ php app/console doctrine:fixtures:load -–env test

Creando los tests funcionales

En este punto ya podremos crear el código que simulará la actuación del usuario sobre las funciones de nuestro sistema y validar los resultados obtenidos. Si coinciden con lo esperado el test pasará, de lo contrario devolverá un error. Os pongo a continuación parte del código de uno de mis test, que servirá para explicar el funcionamiento; no lo pongo completo porque me parece irrelevante; en la documentación oficial de Symfony puedes obtener más información sobre cómo realizar test funcionales.

//El script original testa los distintos métodos http (GET, POST, PUT, DELETE) de un servicio rest que realiza operaciones CRUD, aunque se ha facilitado para mostrar una simple comprobación GET

namespace JVC\MainBundle\Tests\Controller\MailControllerTest;

use JVC\MainBundle\Component\WebTestCaseFactory;
use JVC\MainBundle\Component\WebTestCaseFactory\ConcreteFactoryInterface;

// WebTestCaseFactory es una factoría abstracta que utilizo de ayuda para centralizar funciones recurrentes en los tests como el login de usuario
class ApiMailControllerTest extends WebTestCaseFactory implements ConcreteFactoryInterface
{
public function __construct() {
$this->baseUrl = '/api/mail';
}
public function testGetBase() {
$this->client->request('GET', $this->baseUrl);
parent::assertSuccess(); //Devuelve OK si el usuario puede acceder al recurso
//Comprobamos que el valor que introducimos en nuestro fixture aparece en la respuesta
$this->assertRegexp('/MAIL_TEST/', $this->client->getResponse()->getContent());
}
}

Por último sólo nos queda lanzarlo con phpunit:

$ phpunit –c app