Día 2. Unit Testing - Tests Unitarios

Día 2. Unit Testing - Tests Unitarios

Ayer vimos una primera definición de qué es el testing automatizado y elegimos vitest como motor de testing. Hoy vamos a adentrarnos en el testing, empezando con los tests unitarios. Recuerda que para utilizar vitest debemos instalarlo y añadir el script “test”: “vitest” a nuestro package.json. A partir de este momento podemos empezar a escribir […]

Categorías: Cursos

Adrián Garzón Ximénez - Desarrollador Fullstack


Ayer vimos una primera definición de qué es el testing automatizado y elegimos vitest como motor de testing. Hoy vamos a adentrarnos en el testing, empezando con los tests unitarios.

Recuerda que para utilizar vitest debemos instalarlo y añadir el script “test”: “vitest” a nuestro package.json. A partir de este momento podemos empezar a escribir nuestros tests. Para ello, basta con crear archivos con la terminación .test.js. Vitest los localizará y los lanzará cuando ejecutemos el comando test.

Cómo escribir nuestro primer test

Supongamos que partimos de la siguiente función, que suma todos los números que le pasemos como argumentos:

export const add = (...numbers) => numbers.reduce((a, b) => a + b, 0);

Lo primero que necesitaríamos hacer es crear un archivo add.test.js. Si al crearlo lanzamos el comando npm test veremos que obtenemos un error. Es vitest diciéndonos que todavía no hemos creado ningún test. Pongámonos manos a la obra.

test o it

Lo primero que necesitamos para ejecutar un test es una función test() o it(). Podemos importarlas de vitest, y ambas se comportan igual.

Tanto test como it son una función que espera dos argumentos:

  1. Una string, con el mensaje del test. Este nos servirá para identificar qué ha fallado o funcionado al lanzar el comando npm test.
  2. Una función, que será el test a lanzar. Dentro de esta función utilizaremos el código a testear, y la terminaremos ejecutando otra función especial: expect.

expect

También es una función que se importa de vitest. En este caso, el argumento que espera es el resultado del código a testear. Este argumento va a representar el valor real arrojado por nuestro código.

A expect podemos encadenarle métodos que sirven para describir nuestras expectativas. Por ejemplo:

import { it, expect } from "vitest";

import { add } from "./add";

it("should add two numbers", () => {

  expect(add(1, 2)).toBe(3);

});

Esta descripción es nuestra aserción. Vitest utiliza las aserciones de Chai, ya que también lo hace Jest, librería con la que es compatible.

El funcionamiento de un test es el siguiente: si el resultado obtenido no se corresponde con el resultado esperado, el test fallará. Al fallar, nos dará información detallada en la consola, incluyendo:

  • El nombre del test.
  • Los valores esperado y recibido.
  • La causa del error.
  • Otra información complementaria, como el número de archivos con tests, de tests ejecutados, la duración de las comprobaciones…

En el caso de que pasemos el test tendremos una información similar aunque menos detallada, porque el test runner no tendrá que informarnos de la razón de nuestros errores.

¿Cómo debemos escribir nuestros tests?

Como en cualquier otra disciplina, a la hora de escribir un test existen ciertas premisas que deberíamos seguir. En primer lugar, debemos tener en cuenta que ciertas cosas son testeables mientras que otras no.

En segundo lugar, respecto a aquellas que podamos testear, debemos seguir buenas prácticas si queremos escribir tests efectivos y útiles.

¿Qué debemos testear?

En general, debemos testear todo aquel código del que seamos responsables. Esto significa que no tenemos por qué hacer tests de paquetes de terceros, APIs del navegador o del framework, etc.

A la hora de elegir un paquete debemos confiar en que el equipo escribió buen código e hizo los tests pertinentes. Entre otras cosas porque, aunque encontráramos un bug, no podríamos resolverlo. De modo que no tiene sentido testear aquel código sobre el que no podamos hacer cambios.

Tampoco deberíamos testear la integración de nuestro backend y frontend desde el propio frontend. En principio bastaría con realizar buenos tests del backend y buenos tests del frontend para garantizar el adecuado funcionamiento de la aplicación.

De hecho, para estos casos solemos hacer mocks para las peticiones a la API y sus respuestas. Esto nos permitirá testear cómo reacciona nuestra aplicación a diferentes errores y respuestas, que es en lo que debemos centrarnos.

Organización

Ten en cuenta que cuando desarrollamos un proyecto nos podemos encontrar con cientos o miles de tests. Por eso debemos tratar de ser organizados a la hora de estructurarlos. Nuestros archivos test.js deben estar o bien agrupados o bien cerca de las unidades que testean.

Además, podemos utilizar la función describe, que también viene incorporada en vitest, para crear una suite de tests. Como it o test, describe espera un primer argumento que describa la suite El segundo argumento será una función, en cuyo bloque de código ejecutaremos todos nuestros tests.

Simplicidad

A la hora de escribir nuestros tests, deberíamos mantener un código sencillo. Basta con probar la funcionalidad básica de la unidad de código, ya que necesitaremos tener un test legible y sin complejidades innecesarias.

Además, cada test debería comprobar solo una reacción o estado del código. Si un test que falla comprueba varias cosas diferentes, podemos tener dificultades para encontrar cuál de ellas está causando el error.

Separación de funciones

Algo que puede ayudarnos a simplificar nuestros tests es separar nuestras funciones. Si cada función se encarga de una única tarea, nuestros tests serán más sencillos.

Como sabes, esto es una premisa básica a la hora de obtener código de calidad. De modo que implementar el testing automatizado en nuestro flujo de trabajo puede ayudarnos a escribir un código más limpio.

Multiplicidad

Otra de las prácticas que queremos seguir es que los tests comprueben diferentes escenarios. ¿Qué pasa si uno de los parámetros de mi función llega indefinido? ¿Y si es un NaN?

Comprueba también los errores. Ten en cuenta que expect() acepta un método .toThrow() que nos permitiría comprobar si nuestro código arroja un error.

Recuerda que un mismo test puede tener varias aserciones. Sin embargo, en general es mejor que escribas varios tests para mantener la simplicidad. Nuestros tests solo deberían incluir varios expect() si todos ellos están testeando la misma funcionalidad. Verificar diferentes comportamientos no supone abandonar la simplicidad de nuestros tests.

¿Cómo testear los errores?

Para testear un error podemos utilizar expect().toThrow(). El problema es que, en este caso, el argumento que tendremos que pasar a expect debe ser una función que, al ejecutarse, será la que lance el error.

Dicho de otro modo, tenemos que envolver la función o unidad que estamos testeando en una callback function. Expect se encargará de ejecutarla para capturar el error en caso de que este sea arrojado.

Esta forma de testear nos permitirá comprobar también detalles como el mensaje de error o su tipo.

El patrón AAA - Arrange, Act, Assert

A la hora de escribir nuestros tests, existe un patrón al que conviene que prestemos atención. Se trata del patrón AAA:

  1. Arrange (preparar). Lo primero que debemos hacer es preparar la información que necesitaremos para hacer el test.
  2. Act (actuar). A continuación deberemos ejecutar el código que queramos testear.
  3. Assert (evaluar). Por último, evaluaremos el resultado obtenido y lo compararemos con el esperado.

Mejor calidad que cantidad

Existen métricas como la cobertura del código que nos ayudarán a saber qué proporción de nuestro código está sujeta a verificaciones. Pero ten en cuenta que podemos alcanzar un 100% de cobertura de código habiendo hecho tests nefastos que no sirven para nada.

De modo que, aunque la cobertura es un factor importante:

  1. No deberíamos perseguir como objetivo una cobertura del 100%. Hay razones para no testear partes del código, como que ya se haya testeado en otras partes del proyecto.
  2. La cobertura del 100% no indica que el 100% de los tests sean útiles. Prioriza la calidad, porque puedes tener un proyecto repleto de tests que no sirvan para gran cosa.

Una nota sobre los tests de integración

¿Qué son los tests de integración? Como hemos visto, se trata tan solo de tests que comprueban cómo funcionan varias unidades en conjunto. Por tanto… ¿debemos crear algún tipo de test especial?

En realidad no. Basta con que integremos la unidad de código que estamos testeando en un entorno más realista. Al probar el conjunto de unidades, siguiendo las prácticas que hemos visto, estaremos haciendo tests de integración.

Por ejemplo, cuando trabajamos en React (o cualquier otro framework similar) y renderizamos un componente, normalmente lo renderizamos entre otros componentes. Al testear que un botón se renderiza en nuestra homepage ya estamos haciendo un test de integración, pues probablemente ese botón vaya dentro de otros componentes (un formulario, el navbar, una sección con un CTA…) y por tanto no estamos testeando el componente en solitario, sino integrado en un sistema.

Lo mismo ocurre si testeamos una función que, a su vez, llama a otras funciones. Lo idóneo es que cada una de estas funciones se compruebe por separado (unit testing). Luego basta con testear la función “padre” siguiendo los mismos patrones y estaremos haciendo integration testing.

Curso de introducción al testing

Visita mi repositorio intro-to-testing para ver ejemplos de todo lo que hemos visto a lo largo de este curso:

  1. ¿Qué es el testing?
  2. Unit testing
  3. Conceptos intermedios de testing
  4. Cómo hacer tests en el DOM