Tests logo
Getting your Trinity Audio player ready...

Tests

A día de hoy, muchas veces el software falla porque lo programan personas, por lo que es factible que tenga errores. Así que como desarrolladores y desarrolladoras, debemos siempre intentar minimizar esos fallos. Para ello disponemos de varias herramientas, y una de las más útiles son los tests. Estos pueden ser automáticos o manuales.

Si hablamos de la vertiente automática, se puede definir un test como un bloque de código que permite aseverar el cumplimiento de determinadas afirmaciones. A su vez, cada afirmación viene dada por una sentencia, en la que un método o una función comprueban si un valor cumple con las expectativas.

¿Cómo deberían ser los tests?

A la hora de testear podemos seguir algunas directrices:

Confianza en el código
Es importante que pruebes todo lo que sea necesario para tener confianza en el código desarrollado. No significa que se deba probar hasta el más mínimo resquicio, sino que pruebes todos los comportamientos que se esperan.

Reducir el código malo
Usa los tests para reducir en la medida de lo posible el mal código. Por ejemplo, si surge un error, deberías crear un test que asegure que éste problema no se vaya a repetir en el futuro. Además, estos nuevos tests te ayudarán a documentar el error.

Prueba una cosa, afirma varias
Incluye todas las afirmaciones que sean necesarias en un test. Lo importante es asegurarnos de que todas las partes afectadas por el test tengan los datos correctos.

Los tests primero
Deberías definir los tests a realizar antes de programar. Esto significa que es buena idea pensar el enunciado de los tests, para tener una visión global de la funcionalidad a desarrollar. Además, ayudará a entender mejor que se debe hacer.

Ejecuta los tests
Parece una obviedad, pero es muy importante que una vez que exista un test se ejecute siempre. Por ejemplo, los tests unitarios deberías ejecutarlos al menos, antes de hacer un commit y antes de hacer un deploy a un entorno de test.

Lanzar los tests necesarios en los momentos necesarios
Es importante que aprendas a ejecutar un subconjunto de tests unitarios, ya que aunque los tests sean rápidos por si sólos, en cuanto haya unos cientos, el tiempo de ejecución total puede ser bastante alto. Es por ello, que mientras desarrollas debes saber como poder ejecutar sólo los tests que estés modificando, para ahorrar tiempo. Esto no quita que, de vez en cuando, ejecutes toda la suite de tests para asegurarte de que todo sigue en su sitio.

Automatiza la ejecución de tests
A día de hoy resulta casi impensable que haya un sistema de CI/CD (Integración continua, despliegue contínuo), que durante el proceso no ejecute los tests, ya sean unitarios, de integración, etc.

Tests en las revisiones
Es buena idea comenzar a ver los casos de tests que has creado en las revisiones de código. Por un lado, junto con la definición de la historia, puede ayudar a quienes van a revisar el código a entender si se cumple el comportamiento de la funcionalidad desarrollada. Por otro, seguro que te obliga a revisar mejor los tests antes de enseñarlos a otras personas. Y si no haces revisiones de código, deberías comenzar cuanto antes 😉.

Nomenclatura de los tests

Una de las primeras recomendaciones al escribir el nombre de los tests, es comenzar a utilizar la palabra "should" ("debería" en castellano). Puede parecer una tontería, pero su semántica nos puede ayudar a entender el por qué: cuando indicamos, por ejemplo, un nombre del estilo "debería hacer X cuando Y", éste ya nos está ayudando a pensar si realmente esa funcionalidad es del componente a testear. Incluso si a futuro la funcionalidad ha cambiado, tendrá todo el sentido del mundo ver que el nombre del test nos "pregunte" si debería seguir ocurriendo lo que hacía.

Si vemos que no se puede escribir un test bajo esa premisa, quizás sea porque éste debería pertenecer a otro elemento en el código: una clase nueva, otra función, etc.

Y es que a la hora de redactar pruebas, se comprueba como un test con un buen nombre brilla en los momentos en los que éste falla. Si el nombre no te aporta nada cuando el test falla, debes darle una vuelta para mejorarlo.

Pensemos por ejemplo en la típica funcionalidad que nos envía un correo de confirmación al cambiar nuestra contraseña. Si tengo una clase que gestiona la autenticación de usuario, no tiene sentido tener un test: "debería autenticarme con el usuario X, acceder a su configuración, cambiar la contraseña y recibir un email de confirmación". Con esto, vemos por ejemplo, como el nombre del test nos da la pista de que esa clase está haciendo demasiadas cosas, lo que incumple la S de los principios SOLID, llevándonos a liberarla de responsabilidades.

Flujo de test en cuatro fases

A la hora de escribir tus pruebas viene bien seguir el patrón de test en cuatro fases, que nos define qué flujo de ejecución debería seguir cada test:

  • Setup: fase en la que se construye todo lo necesario para que el test tenga listo el entorno de su ejecución.
  • Exercise: pasos para que el test se ejecute. Por ejemplo: instanciar una clase y llamar a varios de sus métodos.
  • Verify: comprobación del resultado esperado tras ejecutar los pasos de Exercise.
  • Teardown: fase opcional en la que se restaura todo lo modificado por el test. Normalmente, esto aplicará más a estructuras compartidas globalmente, mocks estáticos, etc.

TDD

A finales de los 90 del siglo pasado Kent Beck, como parte de Extreme Programming, desarrolló y especificó la técnica TDD (Test-Driven Development o Desarrollo Guiado por Test). Básicamente, se basa en construir el software a través de la escritura de tests, definiendo tres sencillos pasos a seguir basados en los colores de los semáforos:

Fase roja: escribir un test para la próxima funcionalidad a añadir (inicialmente fallará). 
Fase verde: desarrollar la funcionalidad con el código más simple que haga que el test pase. 
Fase ámbar: refactorizar el código hasta que esté bien estructurado.

Beneficios de TDD

TDD pone el foco en el comportamiento (recordad esta palabra) del código y no en su implementación, puesto que al desarrollar con TDD se escribe cada test antes de su correspondiente código de producción.

Seguir esta orientación en el desarrollo te proporciona varios beneficios:

  • Vas a escribir código como respuesta a que un test pase. Esto significa que el código va a estar bien testeado y el código no hará más de lo que debe.
  • El segundo es que este planteamiento te hará pensar primero en la interfaz del código, antes de que en su implementación. Lo que te ayudará a tener mejor estructurado el código y sólo hacer público lo que sea necesario.
  • Finalmente, como resultado a los dos beneficios anteriores, tenemos un código con menos errores. Y es que en el estudio "Does Test-Driven Development Really Improve Software Design Quality?" de David S. Janzen y Hossein Saiedian, ya se indica como con TDD se reduce sustancialmente el número de errores que aparecen en el software al aplicar la técnica.

Flujo de desarrollo en TDD

  • Momento de pensar: por desgracia es la fase más olvidada en los flujos de desarrollo, y es que antes de escribir una sóla línea de código, habría que detenerse a pensar cuál es el objetivo del código que vamos a desarrollar, que posibles restricciones o casos especiales hay, etc. Todo esto nos ayudará a tener un esquema del comportamiento esperado para la funcionalidad a crear, y evitará los cambios de última hora porque no tuvimos en cuenta el alcance de lo que se quería desarrollar.
  • Escribe el primer test: una vez que tenemos claro el objetivo a conseguir. Escribiremos el primer test... Sí, aún no hay nada de código, pero el test usará la interfaz que nos gustaría crear, esto te ayudará a pensar no en la implementación del código, sino en la interfaz que como "agente externo" te gustaría usar. Este primer test, como es lógico, va a fallar.
  • Hora de programar: ahora es el momento de implementar el esqueleto que usaste en el test, hasta hacer que éste pase.
  • Refactoriza: una vez que los tests de la funcionalidad que querías desarrollar se ejecuten correctamente, es el momento de revisar y refactorizar el código para que quede lo mejor posible. Sobra decir, que tras refactorizar, los tests deben seguir terminando de forma satisfactoria. Eso sí, limítate a refactorizar el código actual, no pienses en futuras mejoras (que puede que nunca lleguen), recuerda que refactorizar no significa cambiar el comportamiento del código.
  • Volver a la casilla de salida: cuando tengas una nueva funcionalidad, tan sólo tendrás que repetir los pasos.

La clave principal de este flujo de desarrollo, es realizar pequeños cambios de forma incremental. Por lo que en todo momento, se espera que el código escrito esté bien testeado.

Legacy code y TDD

TDD no sólo sirve para desarrollos nuevos. En el caso de tener que modificar legacy code (código antiguo u obsoleto y que normalmente no tendrá tests), TDD nos permitirá seguir un flujo que ayude a comprobar este código.

  • Primero escribiremos los tests para verificar que se comprueba lo que hace el código antiguo.
  • Una vez que tenemos los tests, ya podemos comenzar a añadir o corregir las funcionalidades.
  • Si es una nueva funcionalidad seguiremos el flujo habitual.
  • En caso de que se vaya a corregir un comportamiento erróneo, se partirá por retocar el test que se hizo pasar con el comportamiento erróneo, para adaptarlo a la corrección. A partir de ahí, el test fallará y ya podremos comenzar con nuestro ciclo TDD habitual.

Conclusiones

TDD plantea muchas ventajas a la hora de desarrollar:

  • Permite pensar primero antes de escribir una sola línea de código.
  • Ayuda a definir una interfaz útil basada en lo que es necesario.
  • Permite reducir sustancialmente el número de errores.
  • Mejora la mantenibilidad del software.

Por contra, su mayor inconveniente es el coste de acostumbrarse a aplicarlo. Por lo que inicialmente desarrollarás más lento. Pero a la larga, aparte de mejorar en velocidad, podrás comprobar como el tiempo de desarrollo global se reduce, puesto que estarás introduciendo menos errores en el código, de forma que no se perderá tanto tiempo en mantenerlo.

Una vez que conocemos los conceptos de TDD, el siguiente paso será ir a por BDD...

Comparte este artículo con quien quieras
Cosas que quizás no sabías de CSS
Plantilla de proyecto TypeScript

Discussion

  • Commenter's Avatar
    Jose Gisbert — 12 diciembre, 2022 at 10:34

    Muy buen artículo Jose. Yo, por suerte, ya conozco muchas de las cosas de las que hablas, . ¿Estaría guay que pusieses algún ejemplo, quizás? ¡Me ha gustado mucho la función de que te lea el artículo en voz alta!

    • Commenter's Avatar
      jose — 12 diciembre, 2022 at 17:49

      Buenas José, me encanta verte por aquí jajaja. Pues lo que comentas no es mala idea. Quizás le haga una actualización en los próximos días para incluir algunos ejemplos, aunque sea en pseudocódigo para que a cada persona le ayude a pensar en la adaptación a su propio lenguaje.

Leave a Comment

Your email address will not be published. Required fields are marked *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.