TDD en React JS usando Jest y React Testing Library

Qué es Test Driven Development (TDD)

Test Driven Development (TDD) es una técnica de desarrollo que consiste en crear código a través de la repetición de un ciclo (flujo) llamado Red/Green/Refactor.

Dos galerías con el mismo componente pero diferentes tamaños

Las fases consisten en:

  • Primero hacer un test, sabiendo que va a fallar (RED).
  • Generar el código de nuestra aplicación MÍNIMO para que el test anterior pase (GREEN).
  • Aplicar code clean y otras buenas prácticas para mejorar nuestro código (REFACTOR).

Beneficios del TDD

Muchas veces, cuando empezamos un proyecto, empezamos a picar código, crear nuestros componentes, servicios y muchas otras cosas. Hasta ese momento todo son risas y alegría 🤪.

Pero llega un momento que queremos aplicar tests, y vemos que hemos generado tal cantidad de código que es una tarea un tanto farragosa. Ahí llegan las lágrimas 🥲 😆

Ahí es donde el TDD brinda sus beneficios:

  • No hará generar código que será más fácil de testear (al empezar por los tests).
  • Evitaremos añadir añadir complejidad accidental al proyecto, ya que crearemos el código mínimo necesario (principio YAGNI: No se implementa en el momento, se implementa cuando se necesita).
  • Código de alta calidad.
  • Nos aseguraremos de tener la seguridad de que si rompemos una funcionalidad, nuestros tests nos avisarán y no los usuarios 🥴.

En este artículo voy a asumir que tienes experiencia con React y Jest, es importante para que algunos conceptos que se aplicarán los tengas claros de antes.

Empezamos 🚀 🚀 🚀

Presentación del proyecto para hacer TDD

Para este ejemplo voy a crear una aplicación muy sencilla en la que tendremos un título y una lista te varios elementos. Algo muy sencillo pero que te ayudará a enteder el concepto TDD, ya que se necesita, de alguna manera, cambiar el chip.

Configuración del proyecto

Para empezar, crearemos el proyecto con Create React App (CRA), que es la forma más sencilla, rápida y directa para empezar un proyecto en React (si no quieres pelear con webpack, babel…).

Por tanto, vamos a la terminal y ejecutamos:

npx create-react-app tdd-testing

Con CRA ya tenemos configurado el entorno de Jest y React Testing Library, las herramientas para test que vamos a ussar.

Estructura del proyecto

Cuando empezamos con un proyecto React con CRA tenemos la siguiente estructura:

Estructura inicial proyecto React con CRA

Lo primero que voy a hacer es limpiar un poco y eliminaré archivos que no usaré, como puede ser el logo y estilos, quedando así:

Limpieza de archivos innecesarios

Con todo esto, ya tenemos listo el proyecto para empezar a hacer TDD.

Fase 1: RED: Nuestro primer test con Jest y React Testing Library

Lo primero, decir que todos los tests los vamos a guardar en una carpeta llamada __tests__ que estará dentro de src.

Así pues, vamos a empezar con nuestro primer test, que será de la página inicial (home-page.js).

Para ello, creamos la carpeta anteriormente comentada y su test: src/__tests__/home-page.test.js

Recuerda, que los tests tienen que tener SIEMPRE el sufijo .test, por ejemplo, home-page.test.js, component.test.js

// src/__tests__/home-page.test.js

import React from "react";
import { render, screen } from "@testing-library/react";

import HomePage from "../components/home-page";

describe("Home Page mount", () => {
  it("must display the home page title", () => {
    render(<HomePage />);

    const title = screen.getByText(/my quotes/i);

    expect(title).toBeInTheDocument();
  });
});

Con nuestro primer test queremos comprobar que el título my quotes realmente existe en el componente. Fíjate que uso una expresión regular y el comodón i para que no distinga entre minúsculas y mayúsculas. Serviría de la misma porma poner el string que estás buscando.

Este test ahora mismo tiene que fallar 👎🏻 porque estamos en la primera fase del TDD y obviamente, todavía no hemos creado nada de código, ni siquiera el archivo que importamos con import HomePage from "../components/home-page";.

Ejecutamos los tests en la terminal:

npm test

Y obtenemos el siguiente error:

Fase 1: RED. Primer test con error

El primer fallo que tenemos, es lógico. Intenta importar un archivo que NO existe. Entonces vamos a ello, creamos en la carpeta components el archivo home-page.js con lo mínimo necesario para continuar con el test:

// src/components/home-page.js

import React from "react";

const HomePage = () => <div />;

export default HomePage;

Y ejecutamos de nuevo el test.

Fase 1: RED. Primer test con error

Como verás el error ya cambia y nos dice que no encuentra el elemento que intentamos buscar (Unable to find an element with the text: /my quotes/i.).

Hasta aquí llega la primera fase del flujo TDD en la que hacemos que un test falle.

Fase 2: GREEN: Hacer que la prueba pase

Como ya hemos comentado anteriormente, ahora tenemos que hacer que este test pase implementando lo mínimo necesario.

Así pues, vamos a modificar nuestro componente de la siguiente manera:

// src/components/home-page.js

import React from "react";

const HomePage = () => <h1>My quotes</h1>;

export default HomePage;

Volvemos a ejecutar el test y ahora SÍ que debería pasar ya que hemos añadido el mínimo necesario que el test requiere para funcionar, un título que contenga My quotes.

Fase 1: RED. Primer test con error

¿Porqué se hace lo mínimo necesario?

Hacer lo mínimo necesario nos mantiene enfocados en que el código tenga justo lo que debe tener, y no hacer optimizaciones HASTA que sea realmente necesario. Un error muy común en los desarrolladores que sobre piensan demasiado las cosas.

Fase 3: REFACTOR: Aplicamos refactor

El refactor se puede aplicar tanto en el código del test, como en el código de la implementación.

Ahora mismo tenemos muy poco código para poder aplicar refactor. Por lo que esta fase no tiene ahora mismo mucho sentido. Vamos a ir creando más tests y lo veremos más adelante.

Listado de citas aplicando TDD

Voy a aplicar una técnica llamada fake it until you make it que consiste en crear pruebas que pasen a pesar de que mi implementación use datos “hardcodeados” o información falsa e iré iterando hasta terminar mi implementación real terminada.

Primero haremos un test para verificar que exista una lista con tres frases (RED).

Luego haremos que pase el test con lo mínimo necesario (GREEN) y datos mockeados (todavía no consumiremos la API).

Para este punto ahora que se podrá hacer un refactor mayor en el código.

Fase 1: RED: Comprobación del listado

Vamos a crear un nuevo test para que falle:

// src/components/home-page.js

describe("Quotes List", () => {
  it("must display 3 quotes", async () => {
    render(<HomePage />);

    const listItem = await screen.findAllByRole("listitem");

    expect(listItem).toHaveLength(3);
  });
});

Ahora queremos comprobar que efectivamente existe un listado de 3 elementos. Para ello usamos la query de findAllByRole que retorna una promesa. Esta promesa es resuelta con el await.

Básicamente los tipos de queries son:

  • getBy*: obtenemos un elemento que sabemos que SÍ existe en el DOM.
  • findBy*: devuelve una promesa que se resuelve cuando encuentra el elemento buscado.
  • queryBy*: buscamos un elemento que puede, o no, existir. Si no existe devuelve null.

Te recomiendo darle un vistazo a la documentación oficial sobre los tipos de queries en React Testing Library.

Lanzamos el test y tenemos nuestro error (RED):

Fase 1: RED. Listado de elementos

Fase 2: GREEN: Hacer que el test del listado pase

Ahora vamos a hacer el MÍNIMO necesario para obtener nuestro GREEN en el test. Vamos a nuestro componente y crearemos el listado que esperamos obtener en el test anterior, quedando de la siguiente manera:

// src/components/home-page.js

import React from "react";

const HomePage = () => (
  <>
    <h1>My quotes</h1>
    <ul>
      <li>Frase 1</li>
      <li>Frase 2</li>
      <li>Frase 3</li>
    </ul>
  </>
);

export default HomePage;

Ahora SÍ el test tiene que pasar por que encuentra un listado (findAllByRole("listitem")) de tres elementos.

Lanzamos el test de nuevo:

Fase 2: GREEN. Listado con el mínimo necesario

Fase 3: Refactor para listado

Un code smell que se puede ver en el test es que se repite dos veces el render(<HomePage />); en cada una.

Así que vamos a usar beforeEach para que en cada test, nos renderice el HomePage, sin necesidad de tener que hacerlo individualmente.

Quedando nuestro test de la siguiente forma:

// src/__tests__/home-page.test.js

import React from "react";
import { render, screen } from "@testing-library/react";

import HomePage from "../components/home-page";

beforeEach(() => {
  render(<HomePage />);
});

describe("Home Page mount", () => {
  it("must display the home page title", () => {
    const title = screen.getByText(/my quotes/i);

    expect(title).toBeInTheDocument();
  });
});

describe("Quotes List", () => {
  it("must display 3 quotes", async () => {
    const listItem = await screen.findAllByRole("listitem");

    expect(listItem).toHaveLength(3);
  });
});

Y todo deberia seguir pasando correctamente 🚀

Conlusión

Es muy habitual dejar los tests para el final de un proyecto. Con ello, corremos el peligro de tener escenarios en los que nuestra aplicación falla y que no nos hayamos dado cuenta hasta que alguien lo reporta (en el peor de los casos, el cliente 🙈).

Al final este ejemplo TDD es algo sencillo, pero la idea es de obtener una visión de qué es TDD y como aplicarlo.

Mi consejo es empezar a aplicar testing desde el inicio del proyecto, ya sea con o sin TDD, pero te ahorrarás disgustos 🙃

Y esto es todo. Espero que te pueda servir 🙃