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

Automatizando las pruebas unitarias y funcionales en Symfony 4..

Hay personas que estáis siguiendo los tutoriales sobre Symfony, escribiéndome, consultándome, sobre Entidades, Doctrine Query Language, y otras cosas.. ¡Gracias! Esto me anima a seguir escribiendo, a terminar con la planificación que hice de puesta al día con Symfony 4. Así que, aquí estoy de nuevo con otro howto o tutorial, 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).

Imagina que vas a entregar una web, la pruebas y pruebas pensando que todo va bien. Pero llega el día de la puesta en producción, pero como no has leído este post y no has automatizado las pruebas.. 😉 empieza a romperse por los rincones que menos habías pensado. No automatizar las pruebas es bastante habitual en el mundo de las webs, pero no debería de ser así. Así que con este post, repasaremos dando pinceladas a las principales herramientas que nos provee Symfony. Así sí que dormiremos tranquilos cada vez que entreguemos una nueva versión de nuestras aplicaciones web hechas en Symfony.

Un poco de teoría

La idea principal es que con Symfony, todo proyecto web, debe tener una batería de pruebas automáticas. Entonces se ejecutan estas pruebas automáticas antes de guardar y publicar actualizaciones en producción. Es la única forma de trabajar estando seguros de que no se rompe nada. Tradicionalmente en las webs, todos hemos programado, probando sólo con nuestro navegador en la sección de la web que estábamos. Y hemos dado por hecho que era suficiente. Pero así no sabemos si algo en la otra punta de la web puede haberse roto.

Deberíamos seguir siempre el TDD (Test Drive Development), es decir, incluso creando primero los tests. Parafraseando, tratando de evitar aburrirte demasiado.. Imagina que tienes un proyecto 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 a crear 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:

composer create-project symfony/skeleton symfony-automated-testing-tdd
composer require --dev profiler maker server 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 pongo DefaultController. Lo siguiente es el formulario de contacto, que lo vamos a simplificar a dos campos: nombre y mensaje. Con esto que lanzamos desde línea de comandos:

php bin/console make:form

..y le ponemos de nombre ContactType. Lo siguiente es darle forma a los ficheros generados que son los siguientes:

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

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 todo lo controla, 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:

php bin/console 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. Así lanzaré los tests 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

Ahora mismo, como no tenemos programado ningún test, nos saldrá por pantalla un resultado vacío como el siguiente:

Corriendo tests, todavía sin programar..

Lo siguiente a saber es que tenemos dos tipos de tests: los unitarios y los funcionales. Los unitarios comprueban partes bien delimitadas de la aplicación web, es decir, prueban clases de PHP. 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..

Para nuestro caso de ejemplo vamos a hacer 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 acabar 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

Simple y llanamente, es más sencillo ver el código:

<?php

namespace tests\Service;

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);
    }
}

Simplemente crea una instancia de la clase ContactsInfoManager y usa la función que tiene llamada saveContactForm.

Como reza la documentación oficial, por convención, para cada test unitario tendremos una clase en una estructura de directorios paralela. En este caso, tenemos el servicio en el fichero src/Service/ContactsInfoManager.php y tenemos su test unitario de aquí arriba en tests/Service/ContactsInfoManagerTest.php.

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. Así podemos tener un arranque de test funcional para este proyecto en el fichero tests/MainTest.php:

<?php

namespace tests;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class MainTest 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:

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

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/

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

..que podemos consultar en local en nuestro navegador en la dirección http://localhost:8000/coverage/index.html. Podremos desplegar sección a sección, inspeccionando incluso qué funciones están comprobadas y cuáles no.

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.

Compartir..

Dejar un comentario

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