Test unitarios. ¿Por dónde empezar?

 

¿Qué es el testing en software?

Los test: eso que todos conocen, todos exigen y pocos hacen.
Todos estamos acostumbrados a construir nuestra lógica de negocio, comprobar que funciona con unos rudimentarios métodos como probar manualmente que todo funciona correctamente y dejarlo así. Eso está bien. El problema aparece cuando la lógica de negocio empieza a crecer y crecer. Las clases empiezan a tener más métodos y algunos de ellos cambian ligeramente (siempre respetando el principio de responsabilidad única).

De repente, algo empieza a fallar y no sabes qué ha sucedido. No sabes de dónde proviene el error y te agobias. Imagina entonces esta situación pero trabajando en equipo.

Los test son esa herramienta que te salvará de esta situación: se trata de un proceso para verificar la funcionalidad y calidad de un software, con el objetivo de garantizar que funciona correctamente conforme a lo que se ha programado con el fin de entregar software de calidad.

El testing implica una ejecución de unos componentes o módulos software que comprueban que el software funciona tal y como esperamos.

Debe ser paralelo. Es decir, se debe desarrollar a medida que se construye el software para evitar olvidos en un futuro.

Tipos de pruebas

Existen varios tipos de pruebas, dependiendo del enfoque que se les da:

  • Pruebas funcionales: en estas pruebas se incluyen todas aquellas que testean cada una de las funciones del software, individualmente o en conjunto. Entre las más importantes y en las que nos centraremos son las pruebas unitarias, pruebas de integración y pruebas de aceptación.
  • Pruebas no funcionales: en estas se incluye todo aquello referido al rendimiento, usabilidad y confiabilidad del software (pruebas de carga, pruebas de estrés, etc)

1. Pruebas unitarias (Unit Testing)

Son aquellas pruebas que se enfocan en la unidad. Es decir, prueba la unidad más pequeña que puede aislarse lógicamente en un sistema. Estas pruebas se realizan a los métodos de las clases. Sin embargo, hay muchos métodos que no es necesario testear. Por ejemplo: imagina un método cuya lógica de negocio es inexistente (simplemente imprimen un atributo, como los getter) o son métodos de los que no depende el correcto funcionamiento de nuestro software. ¿Para qué sería necesario testearla? Correcto: para nada. 

Los test unitarios tienen el fin de comprobar que un método funciona como nosotros esperamos que funcione. 

Además, se caracterizan porque son independientes a todo lo demás. ¿Qué significa esto? significa que funcionan por sí solas. No deben necesitar absolutamente nada para poder correrlos. Por lo tanto, los test unitarios por tanto deben poder ser repetibles o reutilizables. Nunca podrán depender de nada (ni framework, ni conexiones a base de datos u otras herramientas,etc) y se podrán lanzar unitariamente.

Los test es un campo donde existen multitud de opiniones. Sin embargo, los test unitarios siempre deben cumplir las siguientes premisas:

  • Velocidad de ejecución: deben poder ejecutarse en fracciones de segundo. Imagina que existe miles de pruebas unitarias. Si tienes que ejecutarlas todas una y otra vez, querrás que sea rápido.
  • Aislamiento: deben poder ejecutarse de forma aislada, sin dependencias de otros módulos o factores externos, como bases de datos u otras herramientas.
  • Reiterativa: se refiere a que la prueba siempre devolverá los mismos resultados.
  • Autocomprobada: los test unitarios deben poder comprobar, por sí solos, que se devuelve lo que se espera que se devuelva sin intervención humana.

1.1. Cómo llamar a las pruebas unitarias

Es importante asignar un nombre correcto a cada una de las pruebas. La convención nos dice que deben llamarse siguiendo este patrón:

[nombre del método a probar]_[escenario]_[comportamiento esperado al invocarlo]

Existen muchas nomenclaturas, pero en mi opinión ésta es la más clara.

1.2. Cómo se organizan las pruebas

Antes de entrar a desarrollar una prueba unitaria debemos conocer algo más: el patrón AAA.

Este patrón nos dice que debemos separar el test en 3 partes bien diferenciadas:

  1. Arrange: se inicializan los objetos y se establecen los valores que se necesiten.
  2. Act: se realiza la llamada al método que vamos a probar.
  3. Assert: se comprueba que el método a probar se ha ejecutado tal y como se espera que se ejecute.

Este patrón, además, nos da una serie de ventajas, como la de facilitar la lectura a otros desarrolladores.

Imagina una arquitectura hexagonal. En ella, los DTO y los repositorios NO deberían testearse mediante test unitarios. ¿Por qué? porque acceden a la base de datos. La misión de los repositorios es acceder a la base de datos. Por lo tanto, los repositorios se probarán mediante test de integración (dos o más piezas de software).

Los servicios, por ejemplo, pueden probarse de manera aislada (simulando aquellas llamadas que contengan a otros servicios). De esta manera, probamos que su comportamiento sea el esperado. Hablaremos más adelante de cómo simular todas aquellas llamadas a métodos y herramientas que se usan durante la ejecución del método.

La capa de controladores podría testearse con un test unitario, pero es más comodo probarla mediante un test de integración. Para probar una llamada a la API Rest deberíamos hacerlo, por ejemplo, con un cliente de test en Flask. Se hace una llamada real a la API y se comprueba su correcto funcionamiento. 

Los test unitarios son responsabilidad del desarrollador.

2. Pruebas de integración

Como hemos leído anteriormente, son pruebas en las que se ven implicadas dos o más piezas de software. Los módulos o métodos pueden funcionar correctamente de manera aislada pero quizás conjuntamente no funciona tan bien. Para ello, es necesaria probarlas en conjunto. A esto se le llama prueba de integración. 

Las pruebas de integración son igual de importantes  que las pruebas unitarias. Con ellas podemos detectar errores de manera temprana durante el desarrollo. Más importante lo son con la integración continua y la entrega continua (CI/CD). Al trabajar en equipo, varias piezas escritas por desarrolladores distintos podrían no trabajar conjuntamente de la manera esperada. 

Los test de integración son responsabilidad del desarrollador.

3. Pruebas de aceptación

Son aquellas pruebas en las que se comprueba un ciclo completo de ejecución. Es decir, se prueba desde el inicio hasta el final de la ejecución (pruebas end-to-end)

En estas pruebas no se suele tener acceso a la implementación, por lo que se les suele llamar black box. Solo evalúan las entradas y salidas que produce, sin preocuparse el comportamiento interno. 

Estos test se suelen hacer por el cliente, pero suelen estar diseñados por los desarrolladores.

Se suele utilizar un lenguaje humano, como Gherkin que es un lenguaje específico de dominio (DSL) que son diseñados para resolver un problema muy específico. 

Los test de aceptación son responsabilidad del equipo de QA.