Origami, de Jack Lyons

No hay nada mejor que meterse en la cama con la tranquilidad de saber que has hecho bien el trabajo. Por desgracia, eso no es nada fácil para los desarrolladores web; hay tantas cosas que pueden fallar (navegadores, servidores, librerías…) que es difícil tener la conciencia tranquila, ¿verdad?

Bebé cayéndose dormido
«¡Mi código por fin funciona! ? Bueno, pues ahora aprovecho y… ?» (fuente)

Pues bien, puedes estar tranquilo, porque en esta serie de entradas que estoy preparando para ti aprenderás todo lo que necesitas saber para comprobar que tu código funciona y garantizar que seguirá haciéndolo en el futuro. Como el mundo del testing es muy grande, he decidido que me centraré únicamente en los tests unitarios en WordPress, tanto para la parte de servidor (en PHP) como la parte cliente (en JavaScript). Si quieres que trate otros temas más adelante, pídemelo y decidimos qué hacemos ??.

En esta primera entrada empezaremos con algo ligerito: algunas definiciones sobre el testing y su utilidad. Mi objetivo hoy es que tengas claras las bases para motivarte a aplicar esta técnica en tus proyectos. Con un poco de suerte, quedarás con ganas de la siguiente sesión ?

La importancia del testing

La calidad de tu trabajo es crucial si no quieres tirar por los suelos tu reputación. Y es que no hay nada peor que subir una nueva versión de tu plugin o tema al repositorio de WordPress y que sean tus usuarios quienes descubran que no funciona… y lo sé por experiencia propia, créeme. Por suerte, de los errores se aprende y uno descubre que la solución (o parte de ella) está en el control de calidad y tests automatizados.

Tal y como comenté en una entrada anterior, el ciclo habitual de desarrollo de nuevas versiones de un plugin o tema es más o menos así:

  1. Tienes un conjunto de requisitos/funcionalidades que hay que meter en la nueva versión.
  2. Escribes un poco de código para realizar alguna de esas funcionalidades.
  3. Lo pruebas.
  4. Si falla, intentas corregirlo y repites el paso 3.
  5. Cuando ya funciona, iteras sobre los pasos 2, 3 y 4 hasta que tengas listos todos los requisitos.
  6. Pruebas que todo funcione como es debido y que no se haya roto nada.
  7. Subes el plugin a WordPress.org
  8. Y… ¡error! Alguien encuentra un bug.

El problema de este ciclo es que incluye mucha prueba manual: cada vez que añadimos alguna cosita comprobamos que funcione a mano y, una vez lo hace, nos olvidamos de ella de por vida. El problema es que el código no es estático; va cambiando a medida que nuestro proyecto va evolucionando. Las probabilidades de que cualquier cambio del programa tenga efectos secundarios en otras partes aumentan a medida que pasa el tiempo.

Idealmente, necesitamos una red de seguridad que nos garantice que cualquier cosa que hemos dado por buena en el pasado, seguirá funcionando en el futuro. Esta red son precisamente los tests que escribiremos.

Quien dice una
Quien dice una «red de seguridad», dice un paracaídas ? (fuente)

En función del tamaño del elemento testeado, hablaremos de tests unitarios (pequeños módulos), tests de integración (cómo los diferentes componentes interaccionan entre ellos) y otros tests, como los de aceptación (donde se prueba el sistema como si lo usara un usuario). Sea cual sea el tipo de test que tengamos entre manos, hay que recordar que todos ellos seguirán el mismo patrón de tres fases:

  1. Configuración. Preparas el entorno para que el estado inicial siempre sea el mismo (por ejemplo, cargar una copia de la base de datos donde sólo hay una entrada en WordPress con un cierto título y un cierto ID).
  2. Actuar. Realizas algunas acciones para modificar el estado inicial (añadir una entrada, cambiar el título de otra, eliminar un comentario… lo que sea).
  3. Comprobar. Finalmente, compruebas que el resultado de las acciones realizadas se corresponde con el resultado que esperabas. Así, por ejemplo, si partías de un estado en que había una única entrada en tu blog y la has borrado, el estado final debería ser que no hay ninguna entrada.

Test unitario

Los tests unitarios son el primer paso que, como desarrollador, deberías dar para comprobar que tu código funciona. El objetivo de estos tests es verificar una única pieza del código; normalmente, una función o método de una clase. Como cualquier otro tipo de test, su funcionamiento se basa en comprobar que, dados unos datos de entrada conocidos, el resultado obtenido era el esperado.

Es posible que el párrafo anterior suene un poco abstracto, así que vamos a ver un pequeño ejemplo que nos ayude a entender todo esto. Imagina que tenemos una función que tiene que calcular el IVA de un precio. En España, sabemos que el IVA es del 21%, así que algo que cueste 10€ tendrá un IVA de 2,10€. Para realizar este cálculo, podríamos definir la siguiente función en nuestro plugin:

function nelio_get_iva( $quantity ) {
  return $quantity * 0.21;
}//end nelio_get_iva()

Si hemos programado bien esta función, la llamada nelio_get_iva(10) devolverá 2.1. Hasta aquí muy sencillo, verdad? Pues vamos a complicarlo un poco más. Imaginemos que el valor que pasamos a la función (el parámetro $quantity) es un valor que ha escrito el usuario. Como somos muy majos y no queremos agobiarle con formatos, decidimos que cualquiera de estos valores entrados por el usuario son válidos:

  • Precio sin florituras: 1000.
  • Precio con separador de miles: 1.000. (punto) o 1 000 (espacio)
  • Precio con el símbolo de euro: 15€ o 15 €.
  • Precio con la palabra euro(s): 15 euros o 1 euro o, incluso, 3 EUR.
  • Precio con decimales y sin decimales: 15,00, 15.00,  15 o 15'00.
  • Combinaciones de lo anterior: 1.500,00 EUR o 1200.50.
Porque cualquier ejemplo que se precie habla de dinero.
Porque cualquier ejemplo que se precie habla de dinero.

Si queremos aceptar todos estos valores, ahora en la función ya no podemos simplemente multiplicar el parámetro de entrada por 0.21, porque habrá casos en los que el parámetro de entrada no será un número, sino una cadena de texto con «caracteres raros». Ahora bien, independientemente de qué hayamos hecho para aceptar todos esos valores, sabemos que nuestra función deberá comportarse tal que así:

  • Si el precio es 1000, el IVA es 210.
  • Si el precio es 15 €, el IVA es 3.15.
  • Si el precio es 1.200,50 EUR, el IVA es 252.11.

y eso es precisamente lo que hace un test unitario. Un test unitario lo único que hace es comprobar que, dadas las condiciones iniciales correctas, el resultado es el esperado. En castellano plano, ejemplos de tests unitarios que podríamos aplicar a nuestra función nelio_get_iva serían algo así como:

  • Oye nelio_get_iva, si te paso como parámetro el número 1000, cuál es el IVA? Yo espero que sea el número 210.
  • Oye nelio_get_iva, si te paso como parámetro la cadena de texto 15 €, cuál es el IVA? Yo espero que sea el número 3.15.
  • Oye nelio_get_iva, si te paso como parámetro la cadena de texto 1.200,50 EUR, cuál es el IVA? Yo espero que sea 252.11.

Si cualquiera de estas comprobaciones (en terminología de testing y, por tanto, en inglés, se llaman assertions) es falsa, el test fallará y sabremos que nuestra función no está funcionando como esperábamos. Además, si en el futuro cambiamos la función y disponemos de este conjunto de pruebas, cualquier cambio inesperado (por ejemplo, la función deja de aceptar el . punto como separador de miles) hará que el test devuelva un error, puesto que una de sus comprobaciones fallará y nos enteraremos.

Test de integración

Como acabamos de ver, los tests unitarios prueban partes atómicas de nuestro producto: las funciones (a veces pueden llegar incluso a probar una clase entera). Pero para que nuestro plugin o tema funcione correctamente es necesario que todas las clases y funciones de nuestro sistema funcionen en sincronía, como partes de un único mecanismo. Para comprobar que las piezas operan como es debido tenemos los tests de integración. La mejor forma de explicar la importancia de éstas es con el siguiente tuit:

Divertido, ¿verdad? El ejemplo anterior muestra la importancia que tienen los diferentes tipos de test. Para empezar, podemos suponer que se han realizado tests unitarios de la puerta y del pestillo: en el test unitario del pestillo, se han verificado cuestiones como que, por ejemplo, se puede abrir y cerrar correctamente, o que este movimiento sólo se hace si levantamos el pestillo hacia arriba. En el de la puerta, hemos comprobado cómo se abre y se cierra correctamente también.

El test de integración, por otro lado, es el que hubiera considerado el funcionamiento de ambos componentes dentro del sistema. Es decir, sería un tipo de test que verificaría que la puerta no se puede abrir cuando el pestillo está echado. Pero, ¡ah, amigo!, el ejemplo anterior nos demuestra que esto no es así: el pestillo está echado y, sin embargo, la puerta se abre. Los componentes de manera individual cumplen con los requisitos planteados, pero el sistema no resuelve el problema. Y es que, en este caso, el pestillo y la puerta que hemos usado no son compatibles entre si, con lo que habría que cambiar (o rediseñar) alguno de ellos para satisfacer nuestros objetivos.

Tests de aceptación

El último eslabón de tests que puedes realizar son los tests de aceptación (acceptance tests), los cuales traté en la charla que di en la WordCamp Europa de 2015, en Sevilla. Los tests de aceptación son parecidos a los tests de integración, en tanto que prueban el funcionamiento de las diferentes partes del sistema a la vez. La diferencia principal está en la forma en que se realiza el test.

Los tests de integración tratan al sistema como una caja blanca. Comprueban que las diferentes partes del sistema funcionan de forma síncrona y que los datos fluyen entre ellos de forma correcta. Cuando escribimos tests de integración, sabemos todo lo que está pasando tras bastidores: vemos qué datos se leen y escriben en la base de datos, qué cálculos se realizan, etc.

Los tests de aceptación, por otro lado, testean el sistema como si fuera una caja negra; es decir, ignoran todo lo que está pasando en el código y evalúan el funcionamiento del programa desde una perspectiva de usuario. Usando el framework adecuado (yo he usado Codeception para ello y me encanta), podemos escribir tests que simulan la interacción de un usuario humano con el navegador web. Por ejemplo, un test de aceptación para la funcionalidad de «inicio de sesión» en WordPress podría ser:

  1. Accedo a http://local.wordpress.dev/wp-login.php
  2. Escribo david en el campo usuario y password en el campo contraseña.
  3. Pulso el botón Iniciar sesión.
  4. Se supone que en pantalla tiene que aparecer el texto «Hola, David» (que es el texto que vemos en la esquina superior derecha del Escritorio de WordPress).

Desarrollo dirigido por test (TDD)

No quería acabar esta entrada sin mencionar el desarrollo dirigido por tests (o TDD, por sus siglas en inglés). Si te fijas en lo que te he estado contando hasta ahora, el flujo de trabajo es algo tal que así:

  • escribes una nueva función o pieza de código,
  • escribes el test que comprueba su correcto funcionamiento,
  • ejecutas el test y compruebas que, efectivamente, todo funciona. Si no es así, lo arreglas.

Pues bien, el TDD es una forma de trabajo que propone trabajar del revés:

  • primero escribe el test,
  • a continuación compruebas que falla, lo cual es lógico puesto que el código que haría posible que el test pase aún no existe,
  • luego escribes el código para que pase el test deje de fallar,
  • ejecutas el test y compruebas que, ahora sí, está todo bien. Si no es así, lo arreglas.

Personalmente, nunca he usado esta metodología en ningún proyecto grande; mis intentos siempre han quedado en pequeños fragmentos de código que hacía por mi cuenta, para jugar o practicar. De todas formas, creo que es necesario que entiendas un poco más como funcionan los tests «sin virguerías» antes de ponernos a hablar de estos trucos de profesional ?

Resumiendo

Existen diferentes tipos de test, en función de qué y cómo estamos realizando las pruebas. Independientemente del tipo de test que realicemos, el patrón siempre es el mismo: configuramos el entorno, describimos la situación que queremos y le indicamos qué resultado esperamos. Si el resultado que obtenemos es diferente del que esperábamos, sabemos que algo ha ido mal y habrá que trabajar para solucionarlo. Sino, podemos respirar tranquilos.

En la próxima entrada nos pondremos ya en serio con el tema y aprenderemos cómo realizar tests unitarios en la parte de servidor con PHPUnit. ¡Nos vemos en unos días!

Imagen desatacada de Jack Lyons.

Deja una respuesta

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

He leído y acepto la política de privacidad de Nelio Software

Tus datos personales se almacenarán en SiteGround y serán usados por Nelio Software con el único objetivo de publicar tu comentario aquí. Con el envío de este comentario, nos das el consentimiento expreso para ello. Escríbenos para acceder, rectificar, limitar o eliminar tus datos personales.