Symfony: tutorial 15: los tests automáticos, funcionales y unitarios

Automatizando las pruebas unitarias y funcionales en Symfony..

Aquí estoy de nuevo con otro howto o tutorial de la serie de tutoriales de iniciación a Symfony sobre éste framework tan cojonudo. Hoy llegamos a cómo automatizar las pruebas de tu aplicación web. Es decir, este post va sobre Test Drive Development (TDD para los amigos). Automatizar las pruebas y pasar los tests puede verse como un juego en el que tenemos que conseguir un buen porcentaje o puntos, para estar seguros de que todo funciona. Es bien divertido trabajar con esto.. 😜

Imagina que vas a entregar una web, o una nueva funcionalidad. La pruebas y pruebas, pensando que todo va bien. Pero cada día tiene más cosas, más zonas que comprobar, añadimos algo en un fichero que pensamos que no tiene relación con otra cosa. Pero llega el día de la puesta en producción, y empieza a romperse por los rincones que menos habías pensado 😱😱😱

No automatizar pruebas es bastante habitual en el mundo de las webs, pero no debería de ser así. Espero con este post, presentar a los nuevos qué es esto de la integración contínua, y para los más veteranos dejaré también referencias para seguir avanzando. Ésta es la única manera de trabajar Symfony de forma profesional, es la única forma de dormir tranquilos cada vez que entreguemos una nueva versión.

Antes de seguir, te puedes bajar todo el código fuente de aquí:
https://github.com/jaimenj/symfony-starter-tutorials

O lo que es mejor, si quieres hacer el codekata, puedes ir creando tu proyecto mientras sigues leyendo..

Un poco de teoría

La idea es que con Symfony, todo proyecto debe de tener una batería de pruebas automáticas. Entonces se ejecutan estas pruebas automáticas antes de guardar y publicar actualizaciones en producción, incluso antes de compartir con nuestros compañeros de trabajo las nuevas funcionalidades. También se deben de automatizar estas pruebas en el repositorio de códigos fuentes, de forma que cada vez que hagamos push, no se añadan errores en la aplicación. Es la única forma de trabajar estando seguros de que no se rompe nada.

Tratando de evitar aburrirte demasiado vayamos a ver casos. Imaginemos un proyecto de pruebas con las siguientes secciones:

  • Página de inicio con un formulario de captación de nuevos clientes.
  • El formulario anterior guarda los datos en la BD y te envía un email.
  • Varias páginas corporativas informativas.
  • Login para usuarios.
  • Zona de usuarios logueados con 3 secciones informativas privadas.
  • Formulario de contacto que simplemente te envía un email cuando el usuario llena dicho formulario.

Unos tests unitarios podrían ser:

  • Enviar email de nuevo cliente.
  • Enviar email de contacto.

Unos tests funcionales podrían ser:

  • Bot que envia el formulario de captación de clientes.
  • Bot que comprueba el formulario de contacto.
  • Bot que navega por secciones informativas.
  • Bot que hace login de usuario y navega por las secciones privadas.

La idea es que una vez automatizadas estas pruebas, se las pasas desde línea de comandos cuando quieras. Así si salen bien, ya puedes publicar tus últimos cambios en producción, sin miedo a que algo se rompa. Es muy fácil que modifiques algo en una punta de la web y se rompa en el otro extremo en algo que no pensabas que podía tener relación alguna.

Una página web corporativa de ejemplo

Vamos al grano ahora creando un proyecto de una web corporativa con un formulario de contacto. Dicho formulario de contacto, cuando se rellene y se envíe, guardará los datos en un fichero de texto. Lo resumo así de simple para no mezclar este post con bases de datos, o con envío de correos electrónicos.

Así que comenzamos creando el proyecto:

symfony new symfony-tutorial-15
composer require --dev profiler maker phpunit-bridge browser-kit css-selector
composer require twig annotations form

Por simplificar vamos a crear una única ruta, la página de inicio, como si fuera una web a una página. El formulario lo pondremos abajo al final de la misma web. Entonces, creamos el controlador con la plantilla:

php bin/console make:controller

..y cuando pregunta el nombre del controlador le ponemos DefaultController. Lo siguiente es el formulario de contacto, que lo vamos a simplificar a dos campos: name y message. Con esto que lanzamos desde línea de comandos:

php bin/console make:form

..y le ponemos de nombre ContactType. No lo enlazamos con ninguna entidad, ya que vamos a editarlo manualmente. Lo siguiente es darle forma a los ficheros generados:

  • src/Controller/DefaultController.php
  • src/Form/ContactType.php
  • templates/default/index.html.twig
  • templates/base.html.twig

..y a un servicio que vamos a generar:

  • src/Service/ContactsInfoManager.php

Show me the code..

A continuación dejo los códigos fuentes de la parte básica de una web corporativa a una página. Se tiene que ver tal como la imagen siguiente desde el navegador después de poner los códigos en cada fichero..

Página de pruebas corporativa, haciendo POST en el formulario de contacto..

Vamos por orden, primero el controlador, que está en el fichero src/Controller/DefaultController.php:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use App\Form\ContactType;
use App\Service\ContactsInfoManager;

class DefaultController extends AbstractController
{
    /**
     * @Route("/", name="default")
     */
    public function index(Request $request, ContactsInfoManager $contactsInfoManager)
    {
        $contactForm = $this->createForm(ContactType::class);

        if ($request->isMethod('POST')) {
            $contactForm->handleRequest($request);

            var_dump($contactForm->getData());

            $name = $contactForm->getData()['name'];
            $message = $contactForm->getData()['message'];
            $contactsInfoManager->saveContactForm(
                $name,
                $message
            );
        }

        return $this->render('default/index.html.twig', [
            'controller_name' => 'DefaultController',
            'contactForm' => $contactForm->createView(),
        ]);
    }
}

Necesitamos tener el formulario construido, usando un form type de Symfony. Me remito a cómo se trabajan los formularios en Symfony, esto es material para otro post 😉 el código fuente de este ejemplo en el fichero src/Form/ContactType.php es el siguiente:

<?php

namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;

class ContactType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name')
            ->add('message')
            ->add('send', SubmitType::class, [
                'label' => 'Enviar'
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            // Configure your form options here
        ]);
    }
}

Lo siguiente es que este controlador utiliza una plantilla de Twig, en el fichero templates/default/index.html.twig, con el siguiente código:

{% extends 'base.html.twig' %}

{% block title %}JnjSite.com ejemplo de página corporativa{% endblock %}

{% block body %}

    <div style="background-color: #808080; border-radius: 20px; box-shadow: 5px 5px 5px 0 black; padding: 20px;">
        <h1>Esto es una página corporativa</h1>
    </div>

    <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus ac aliquam lorem. Aliquam quis dui eu elit suscipit venenatis sollicitudin sed est. Proin eu tincidunt justo. Aenean tincidunt nec ex ac auctor. Donec faucibus mauris orci, vitae
        efficitur nibh fringilla hendrerit. Cras gravida eros id diam varius pellentesque. Nulla dapibus dapibus purus blandit blandit. Praesent sodales nulla a felis tincidunt placerat. In in sodales diam. Curabitur blandit, leo ut bibendum rutrum, magna
        justo malesuada tortor, at mollis dui urna sit amet odio. Sed euismod, ante non blandit vestibulum, magna nibh iaculis odio, nec luctus nisl odio non nunc. Duis erat libero, accumsan sed justo in, varius egestas ex. Vestibulum rutrum nulla tincidunt
        risus feugiat hendrerit. Quisque sed quam placerat elit eleifend varius. Pellentesque quam nulla, faucibus eget mollis id, elementum ultrices massa.</p>

    <p>Donec lobortis nunc sit amet mattis tincidunt. Praesent dapibus augue at nulla sagittis, nec ultricies nibh faucibus. Fusce a luctus dui. Praesent venenatis, neque sed condimentum gravida, sem mauris mattis eros, eget malesuada ex ipsum quis erat.
        Etiam mauris odio, malesuada quis convallis at, molestie sit amet enim. In dictum, ipsum id viverra tempus, lorem felis rhoncus lorem, sit amet elementum dolor dui et orci. Phasellus suscipit purus ut ex efficitur consectetur sit amet non magna.
        Praesent tincidunt, est at finibus auctor, nisl nisi pulvinar diam, ut rhoncus lorem nulla iaculis eros.</p>

    <div style="background-color: #aa5; border-radius: 20px; box-shadow: 5px 5px 5px 0 black; padding: 20px; margin: 0px 100px;">
        <h2>Formulario de contacto</h2>
        {{ form(contactForm) }}
    </div>

{% endblock %}

Le he añadido algo de código, y el clásico Lorem Ipsum para tener contenido. Queda ahora un pequeño servicio, creado para construir luego los tests automáticos. Este servicio está en el fichero src/Service/ContactsInfoManager.php y tiene el siguiente contenido:

<?php

namespace App\Service;

class ContactsInfoManager
{
    public function saveContactForm($name, $message)
    {
        file_put_contents(__DIR__.'/../../ficheroDeContactos.txt',
            '======================================='.PHP_EOL
            .'NOMBRE: '.$name.PHP_EOL
            .'MENSAJE: '.$message.PHP_EOL,
            FILE_APPEND
        );
    }
}

Con esto que ya podemos arrancar el servidor de desarrollo local con la instrucción siguiente:

symfony server:start

.. y vemos en http://localhost:8000/ la web. Igualmente podremos enviar el formulario de contacto, que guardará el nombre y mensaje en el fichero ficheroDeContactos.txt añadiendo cada nuevo envío al final del fichero. Tras varias pruebas tendrás un fichero con un contenido parecido al siguiente:

NOMBRE: ajhskjdahj
MENSAJE: kdhasjdhasd
=======================================
NOMBRE: ajhskjdahj
MENSAJE: kdhasjdhasd
=======================================
NOMBRE: ajhskjdahj
MENSAJE: kdhasjdhasd
=======================================
NOMBRE: adsasdad
MENSAJE: asdadasd
=======================================
NOMBRE: ajkshdkjashdkj
MENSAJE: akjdhasdhaskd
=======================================
NOMBRE: ajkshdkjashdkj
MENSAJE: akjdhasdhaskd
=======================================
NOMBRE: ajkshdkjashdkj
MENSAJE: akjdhasdhaskd
=======================================
NOMBRE: ajkshdkjashdkj
MENSAJE: akjdhasdhaskd
=======================================
NOMBRE: ajkshdkjashdkj
MENSAJE: akjdhasdhaskd

Ya tenemos todo preparado para comenzar a crear las pruebas automáticas. Es decir, habitualmente llego en los proyectos a un punto en el que tengo algo funcional, y comienzo a crear los tests automáticos. Lo mejor sería incluso al revés, primero empezar por hacer los tests para luego empezar a construir la aplicación.

Entonces, una vez creemos los tests, los ejecutaremos para asegurarnos de que todo funciona cada vez que vaya a publicar una nueva actualización en producción.

Los tests automáticos, PHPUnit en Symfony

Después de todo lo anterior, y de haber instalado la librería phpunit-bridge, tenemos disponible el siguiente nuevo comando:

php bin/phpunit

La primera vez que lo ejecutemos se instalará todo lo necesario para lanzar nuestra bateria de pruebas:

Lanzanzo PHPUnit la primera vez..

Como hemos dicho antes, tenemos dos tipos de tests: los unitarios y los funcionales. Los unitarios comprueban partes bien delimitadas de la aplicación web, es decir, prueban las capas más internas de la aplicación, bien delimitadas o isoladas. Por otro lado, los test funcionales comprueban la web como si de un usuario se tratara, simulando la navegación, clicks, envío de formularios, etcétera.. Por esta razón puse el post sobre bots web antes de este.

Para este post vamos a ver dos tests: uno unitario que comprueba que se guarda el fichero de datos del formulario. Como ya vimos en posts anteriores, la mayor parte del código debe estar en servicios, y estos servicios se pueden probar con sus tests unitarios correspondientes. Y también vamos a hacer otro test funcional que va a ser una simulación de navegación por la web.

Un test unitario

Ahora también tenemos un generador de código para hacer el esqueleto de los tests, así que vamos a consola y ejecutamos lo siguiente:

php bin/console make:unit-test

..como va a testear la clase ContactsInfoManager, entonces le llamamos ContactsInfoManagerTest y así sabemos con ver el nombre del fichero de qué se trata. Es más sencillo ver el código que explicarlo:

<?php

namespace App\Tests;

use App\Service\ContactsInfoManager;
use PHPUnit\Framework\TestCase;

class ContactsInfoManagerTest extends TestCase
{
    public function testSave()
    {
        if (\file_exists(__DIR__.'/../../ficheroDeContactos.txt')) {
            unlink(__DIR__.'/../../ficheroDeContactos.txt');
        }

        $contactsInfoManager = new ContactsInfoManager();
        $contactsInfoManager->saveContactForm(
            'Nombre de pruebas',
            'Mensaje de pruebas'
        );

        $this->assertEquals(\file_exists(__DIR__.'/../../ficheroDeContactos.txt'), true);
    }
}

Es muy sencillo, este test que he creado simplemente borra el fichero que guarda los datos del formulario si existe, crea una instancia de la clase ContactsInfoManager, y usa la función que tiene llamada saveContactForm. La comprobación de que todo ha funcionado es la última línea con el assertEquals que comprueba que el fichero se ha creado de nuevo. Así sabremos que funciona..

Un test funcional

Ahora vamos con el funcional, para esto vamos a utilizar unas librerías que vimos en un post anterior, en: Symfony: tutorial 14: navegando con DomCrawler, BrowserKit y CssSelector. Es decir, hemos instalado las librerías browser-kit css-selector, así que ahora vamos a navegar por nuestro proyecto local, y nos aseguraremos de que todo funciona.

Vamos entonces a línea de comandos ya que tenemos un fantástico generador de código fuente y hacemos:

php bin/console make:functional-test

Le podemos poner por ejemplo MainFunctionalTest de nombre, y queda en el fichero tests/MainFunctionalTest.php:

<?php

namespace tests;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class MainFunctionalTest extends WebTestCase
{
    public function testShowHomeAndSubmitForm()
    {
        $client = static::createClient();

        $crawler = $client->request('GET', '/');
        $this->assertEquals(200, $client->getResponse()->getStatusCode());

        $form = $crawler->selectButton('Enviar')->form();

        $form['contact[name]'] = 'Jaime';
        $form['contact[message]'] = '¡Hola!';

        $crawler = $client->submit($form);
        $this->assertEquals(200, $client->getResponse()->getStatusCode());
        $this->assertContains(
            '<h1>Esto es una página corporativa</h1>',
            $client->getResponse()->getContent()
        );
    }
}

Mira que las funciones públicas son los tests. Los puntos de control son las aserciones. Cuantos más tests y aserciones mejor. A partir de aquí entra en juego la refactorización contínua de las mejoras. Debemos de ir acomodando y construyendo más tests según vamos añadiendo funcionalidades a la web.

En la siguiente sección se ve cuánto de seguros estamos de que todo funciona..

Gráficos y porcentajes de cobertura

Una de las cosas más molonas son los gráficos. Así estaremos tranquilos de que lo que entregamos funciona. Para el proyecto de este post, llegamos a un 100% de los códigos fuentes construido probados. Aquí es fácil porque es un proyecto pequeño, pero habitualmente deberíamos de alcanzar el 80-90% por lo menos. Así que, ahora mismo, si ejecutamos:

php bin/phpunit

..debemos de ver por pantalla algo parecido a esto en Symfony 4:

Dos tests automáticos, 1 funcional, 1 unitario, 4 puntos de control..

En Symfony 5 el resultado es bien parecido:

Lo siguiente muy útil es saber cuán de seguros estamos de que todo funciona. A mi me gusta usar este comando que genera la convertura:

php bin/phpunit --coverage-html=public/coverage/

Si te da un error puede que te falte alguna librería como a mi, puedes arreglarlo con:

sudo apt install php-xdebug

El argumento –coverage-html lo que hace es generar un informe como el siguiente:

..que podremos consultar en local en nuestro navegador en la dirección http://localhost:8000/coverage/index.html porque lo hemos generado en el directorio /public con el comando anterior. Podremos desplegar sección a sección, inspeccionando incluso qué funciones están comprobadas y cuáles no.

En symfony 5 tiene un aspecto como el siguiente:

Terminando, bibliografía y otros detalles

Ya me estoy extendiendo demasiado para este post que se supone que sólo es introductorio en el desarrollo TDD con Symfony. Me remito a la documentación oficial que está muy bien. Todo hay que decirlo, este tema de las pruebas automáticas es rotundamente necesario para cada proyecto. Todo programador web que se precie de tener un buen nivel profesional en el desarrollo web, debe de tener una batería de pruebas automáticas para cada proyecto.

Aquí también dejo el código fuente de este codekata junto con los demás:
https://github.com/jaimenj/symfony-starter-tutorials

Otro día más.. ¡Un saludo!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *