Pruebas unitarias PHPUnit

En el 2006, trabajé en un ministerio. Mis colegas desarrollaban un sistema maquetado con tablas anidadas. El código boilerplate necesario para, por ejemplo, imprimir un formulario era increíble. Etiquetas <table>, <tr>, <th>, <td>, por doquier. Si tienes, como yo, más de 10 años en esta carrera, sabrás de qué hablo.

Para ese entonces, ya yo sabía maquetar usando CSS, y por supuesto, fervientemente mostré los beneficios de usar CSS. Insistí una y otra vez que debían dejar de maquetar con tablas anidadas.

¿El resultado? Me gané el apodo de “Duilio CSS”: si se colgaba una computadora o se dañaba la cafetera, me pedían que lo arreglara con CSS.

Ahora imagina un 2006 sin HTML5, ni jQuery, y de pronto alguien comienza a crear efectos de fadeIn, fadeOut, etc. Sí, yo lo hice, y fue la única forma de lograr que mis compañeros agregaran hojas de estilo a sus proyectos.

Back to the future

Por supuesto hoy todos usamos CSS, prácticamente nadie maqueta con tablas ni usa etiquetas font; pero en aquel momento, al menos la mitad del departamento de informática, se alegró cuando renuncié.

¿Por qué hablo de CSS en un posts sobre pruebas unitarias?

Creo firmemente que debemos escribir pruebas, tanto como en el 2006 creía en CSS. Estoy seguro que llegará un momento, en 2 o 3 años, donde programar sin pruebas será como maquetar con tablas.

Creo que dentro de no mucho tiempo, en casi todas las solicitudes de trabajo será un requisito indispensable manejar PHPUnit, PHPSpec o sus equivalentes en otros lenguajes.

¿Por qué debemos escribir pruebas automatizadas?

Hace poco, Jeffrey Way dijo en su podcast, que calcula que el 90% de los programadores no prueban su código con pruebas automatizadas. Pero no sólo eso, la verdad es que la mayoría de los programadores no prueban su código lo suficiente, ni siquiera de forma manual. Somos demasiado optimistas.

Desarrollando sin pruebas unitarias

Imagina que el cliente nos pide que agreguemos una función para saludar a los usuarios. Es muy fácil, pensamos, no hace falta escribir ninguna prueba:

function welcome()
{
    return 'Welcome' . auth()->user()->name;
}

En layout.blade.php (suponiendo que estás trabajando con Laravel), llamas a la función de esta forma:

<h1>{{ welcome() }}</h1>

Como es algo tan sencillo y fácil, probablemente tampoco haga falta probar en el navegador, así que subimos el código directamente al repositorio, hacemos deploy y continuamos con la siguiente tarea.

2 minutos después recibimos una llamada del cliente furioso: toda su página está caída.
“Attempting to use the property name on a none object”

Si el usuario no está conectado, auth()->user() va a devolver null, no tomaste la previsión de ese caso. Comienzas a sudar frío y a reparar el error a la carrera, subes una corrección:

function welcome()
{
    if (auth()->check()) {
        return 'Welcome' . auth()->user()->name;
    } else {
        return 'Welcome guest!';
    }
}

Subes el cambio, haces deploy, abres la página y puedes ver el siguiente mensaje: Welcome guest!

Suspiras. Pero 5 minutos después el cliente vuelve a llamar, te pregunta si probaste el código, le dices que sí, el cliente te pregunta si probaste el código estando como un usuario conectado, sí, por supuesto, mientes, mientras corres a conectarte y ves el siguiente mensaje: WelcomePedro!

«Me faltó un espacio» le dices al cliente, avergonzado, ya lo corrijo…

¿De verdad era un código muy sencillo que no requería pruebas? ¿Te sientes identificado con esta historia?

Tipos de pruebas automatizadas y cómo usarlas

Hay varios tipos y subtipos de pruebas, y algunos autores o frameworks usan diferentes terminologías, a veces para referirse al mismo tipo de prueba. Pero de nuevo, no te preocupes, así como para hablar español no necesitas saber que es un subjuntivo, para escribir pruebas no debes ser experto en terminología.

Veamos por ahora dos maneras en que podemos probar nuestra aplicación.

Uso de Pruebas unitarias

La primera forma de comprobar que nuestra función welcome() funciona es… Comprobar que nuestra función welcome() funciona. En otras palabras: probar sólo la función.

Para este ejemplo, podemos instalar Laravel, no es necesario pero Laravel ya trae incluido PHPUnit y nos dará un escenario en el que continuar el tutorial rápidamente.

composer create-project laravel/laravel welcome

También puedes leer: Donde puedo comenzar a aprender Laravel

instalacion-laravel-composer

Una vez finalizado el proceso de instalación, accedemos al directorio del proyecto dentro de la misma consola y usamos el generador de Laravel para crear nuestra primera prueba:

cd welcome
php artisan make:test WelcomeTest

Ahora puedes darle un vistazo al archivo tests/WelcomeTest.php e inclusive ejecutar esta prueba en la consola con:

vendor/bin/phpunit –filter WelcomeTest

¿Qué hace esto?

Básicamente comprueba que true es true. Obvio, ¿No? Sí, pero aún con esta prueba tan básica podemos hacer mucho. Pero primero, cambiemos lo siguiente:

$this->assertTrue(true) por $this->assertTrue(false) y ejecutemos la prueba otra vez:

pruebas-unitarias-assertTrue

En la imagen, puedes ver la diferencia entre la primera vez que ejecutamos la prueba y la segunda.

Por supuesto, lo que pasas como argumento a assertTrue, no tiene porqué ser una constante, puede ser el resultado de una función, etc. Por ejemplo:

$this->assertTrue(welcome() == ‘Welcome guest!’);

Lo anterior también se puede expresar así:

$this->assertSame(welcome(), ‘Welcome guest!’);

Si ejecutamos esto último en la consola, obtendremos este error:

1) WelcomeTest::testExample
Error: Call to undefined function welcome()

Por supuesto la función welcome no existe -aún-. Pero fíjate que lo primero que pensé mientras escribía la prueba, era a quien le iba a dar la bienvenida. Welcome… Who? En este caso “Guest” porque no tengo ningún usuario conectado. Entonces al momento de escribir el código, vamos a tener esto en cuenta. ¡Sí! PHPUnit te vuelve un mejor programador casi por arte de magia.

Nuestra prueba está quedando así:

<?php

class WelcomeTest extends TestCase
{
    public function test_welcome_a_guest_user()
    {
        $this->assertSame(welcome(), 'Welcome guest!');
    }
}

Ahora vamos a definir nuestra función welcome. Para ser rápidos, simplemente copien esto en app/Http/routes.php

if ( ! function_exists('welcome')) {
    function welcome()
    {
        return 'Welcome guest!';
    }
}

También puedes ver: Aprende a crear helpers personalizados en Laravel

Ejecutamos la prueba nuevamente con:

vendor/bin/phpunit –filter WelcomeTest

¡La prueba pasa!

Pero ahora tenemos el segundo escenario: darle la bienvenida al usuario conectado:

Agrega este segundo método a WelcomeTest:

public function test_welcome_a_known_user()
{
    $user = new \App\User(['name' => 'Duilio']);

    auth()->login($user);

    $this->assertSame(welcome(), 'Welcome Duilio!');
}

Volvemos a ejecutar la prueba nuevamente, obtendremos este error:

1) WelcomeTest::test_welcome_a_known_user
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-Welcome guest!
+Welcome Duilio!

Por supuesto, nuestra función siempre devuelve Welcome guest! sin importar qué suceda.

Vamos a acomodar el código dentro de la función welcome:

function welcome()
{
    if (auth()->check()) {
        return 'Welcome ' . auth()->user()->name . '!';
    }

    return 'Welcome guest!';
}

Y ejecutamos la prueba nuevamente:

pruebas-unitarias-phpunit

La prueba pasa, o por el contrario, si olvidamos un espacio, o hicimos algo mal, la prueba nos notificará casi de inmediato.

Ejercicio: agrega una propiedad “gender” a “User” que puede ser “m” o “f” dependiendo del género, ahora el mensaje en español dirá “Bienvenido Juan” o “Bienvenida Ana” dependiendo del caso. ¿Cómo modificarías la prueba y qué cambios harías a la función? Comparte la solución en los comentarios.

Con esto hicimos nuestra primera “prueba unitaria”. Aunque siendo un poco más “estrictos” podríamos decir que esta fue una “prueba de integración” porque combinamos el sistema de autenticación de Laravel + el objeto User de Eloquent + nuestra función de welcome, para poder obtener un resultado. Esto podría cambiar con conceptos como la inyección de dependencias y herramientas como Mockery.

Aprende sobre conceptos avanzados en nuestro curso de creación de componentes.

Pero antes de caer en conceptos más complicados, veamos el segundo enfoque para probar nuestra aplicación:

Pruebas de aplicación

Si bien es cierto que la función welcome funciona perfectamente, si ahora creamos un virtual host y abrimos el navegador sólo veremos la palabra Laravel.

Aprende a crear virtual hosts para Windows y para Linux y Mac.

Nuestras pruebas unitarias o de enfoque unitario, no se dedican a comprobar lo que ve el usuario final en el navegador, sino a probar que pequeñas partes de tu código (o unidades como clases o funciones) cumplen su función.

Pero por otro lado, las pruebas de aplicación sí se enfocan en esto último, y a partir de la versión 5.1 Laravel trae su propio paquete de pruebas de aplicación llamado «Integrated test package».

De hecho si abres el archivo tests/ExampleTest.php verás la primera prueba de aplicación, que viene incluída con el framework:

class ExampleTest extends TestCase
{
    /**
    * A basic functional test example.
    *
    * @return void
    */
    public function testBasicExample()
    {
        $this->visit('/')
          ->see('Laravel 5');
    }
}

En este caso ya no estamos utilizando funciones con el prefijo assert() sino una interfaz más orientada a la perspectiva de un usuario, de un “QA tester”, o de un programador que no escribe pruebas.

Si visito la página / (el home)
Veré Laravel 5.

Si ejecutamos esta prueba en la consola:

vendor/bin/phpunit –filter ExampleTest

La prueba va a pasar. Pero si la cambiamos el texto de Laravel a Welcome Guest!:

$this->visit('/')
    ->see('Welcome guest!');

La prueba va a fallar con un error extenso diciendo que:

Failed asserting that [HTML AQUI] matches PCRE pattern «/Welcome guest\!/i».

Y esto es porque no hemos llamado a la función welcome dentro de la plantilla index de nuestra página web. Piensa un poco: welcome() funciona desde la perspectiva del programador, pero desde la perspectiva del cliente o del usuario no sirve, simplemente porque no puede ver el mensaje de bienvenida en la página.

Corregir esto es muy sencillo, por supuesto:

En resources/views/welcome.blade.php agregamos lo siguiente debajo de la línea que dice Laravel:

{{ welcome() }}

Ejecutamos la prueba otra vez:

La prueba está pasando.

También podemos quitar –filter y ejecutamos TODAS las pruebas de nuestro proyecto:

pruebas unitarias con PHPUnit

En ambos casos las pruebas están pasando, tanto nuestras funciones de forma individual, como las páginas de nuestro proyecto, funcionan.

Como nota final: las pruebas de aplicación también son llamadas muchas veces pruebas de integración, pruebas funcionales (en Codeception) o pruebas de aceptación (acceptance tests).

Las pruebas de aceptación son muy interesantes, porque son básicamente una prueba de aplicación pero con las especificaciones dadas por el cliente: es decir, si la página funciona exactamente como el cliente quiere: la prueba de aceptación pasa. Las pruebas de aceptación pueden escribirse como código o como una simple historia en texto plano que luego se puede traducir o no a la prueba automatizada:

Actuando como el usuario “Duilio”
Cuando visito el index de la página
Quiero ver “Bienvenido Duilio”
Para sentir que la página me aprecia.

Test driven development

También habrás leído: TDD o «desarrollo guiado por pruebas». Suena muy difícil o complicado, ¿Cierto? Sin embargo, ya te lo explique sin que te dieras cuenta:

    1. Escribimos una prueba,la ejecutamos la prueba falla.
    2. Escribimos el código para que la prueba pase
    3. Ejecutamos la prueba, la prueba pasa.
    4. Sino volvemos al punto 2.

Básicamente de eso se trata.

Diferencias entre pruebas unitarias y de aplicación

Si lo más importante es que la página funcione ¿Para qué escribimos pruebas unitarias? ¿Por qué no escribir sólo pruebas de aplicación?

Las pruebas unitarias tienen las siguientes ventajas:

  • Se ejecutan mucho más rápido que las pruebas de aplicación (es más rápido para el computador ejecutar una función que cargar toda una página).
  • Si una prueba unitaria falla, es más fácil de depurar y encontrar el error.
  • Con las pruebas unitarias también podemos cubrir más escenarios y posibilidades dentro de una única función. Mientras que con las pruebas de aplicación a veces es difícil reproducir todos los posibles escenarios de pruebas.

Así que: ¿Cuál es mejor o cuál debo escribir? Ambas. Pero muchas veces basta un simple:

$this->visit(‘planes’)
    ->see('Registrate en Styde')
    ->click('Comprar plan anual') //mensaje subliminal

Para comprobar que tu página abre sin estallar en pedazos. Y en ocasiones cuando tienes una función realmente complicada que cuesta probar en el navegador, las pruebas unitarias caen como WIFI gratis cuando te quedaste sin plan de datos.

Conclusión

La cantidad de tipos de pruebas y conceptos (o jerga) que rodea el mundo de las pruebas automatizadas hace que muchos desarrolladores desistan de usarlas. Pero antes de hablar de los tipos de pruebas que hay, quiero que tengas algo claro: no importa qué tipo de pruebas uses, siempre y cuando las escribas, no importa si no “dominas” todos los conceptos o no eres un “experto” (nadie lo es, todos estamos aprendiendo) lo importante es que comiences a escribir pruebas y que dichas pruebas cumplan su función: comprobar que el código funciona por ti. Sin importar si confundes un mock con un stub o si aún no sabes qué significan esos extraños términos. Siempre que tengas pruebas y éstas respalden la funcionalidad de tu proyecto: vas por buen camino.

Así que ¡Ánimo! Comienza a escribir tus primeras pruebas unitarias o de aplicación, ya mismo, y comparte tus capturas de pantalla con nuestra cuenta de Twitter @StydeNet o a mi cuenta personal @Sileence. También puedes ver nuestro nuevo curso crea una aplicación con Laravel 5.3 donde desarrollamos un proyecto desde cero usando TDD (desarrollo guiado por pruebas).

Regístrate hoy en Styde y obtén acceso a todo nuestro contenido.