Algo que siempre hemos dejado para última hora, o bien dejamos de hacerlo: los tests. Empezaré diciendo que yo también soy nuevo en los tests y que esto será un resumen de lo que voy aprendiendo en el tema testing 😜
Hace unos días entré en un nuevo proyecto y se quiere dejar testeado muchas cosas, con cierto criterio, así que me he puesto a investigar la librería Enzyme con el objetivo de testear algunos componentes de React. La curva de aprendizaje es relativamente sencilla, así que voy a intentar explicar algunos conceptos básico para ir quitándonos el miedo a los tests.
Índice de contenidos:
Lo primero que debemos de mirar es la documentación de Enzyme para la instalación, y aquí haremos una matización.
Si tienes la versión 16 de React, te servirá la documentación actual (este documento lo estoy escribiendo el 8 de febrero del 2021). Si no sabes que versión de React estás usando, ve al package.json
y verás en las dependencias algo como:
Si este es tu caso la instalación sería de la siguiente forma:
npm i --save-dev enzyme enzyme-adapter-react-16
Si tienes la versión 17 de React, tendraás que hacer un pequeño cambio, ya que oficialmente Enzyme no da soporte a la versión 17 (este documento lo estoy escribiendo el 8 de febrero del 2021).
Nota: Si cuando leas este artículo React ya da soporte para la versión 17 no es necesario que hagas la configuración de este modo
Si este es tu caso la instalación sería de la siguiente forma:
npm i --save-dev enzyme
Y luego necesitaremos el adaptador para la versión 17. No es un adaptador oficial, pero Wojciech Maj nos ha dejado uno, de momento, no oficial.
npm install --save-dev @wojtekmaj/enzyme-adapter-react-17
Sólo queda una cosa más. Para poder hacer "capturas" de nuestros componentes y guardarlos en snapshots para hacer ciertas pruebas, vamos a necesitar un paquete que se llama enzyme-to-json
y lo puedes instalar de la siguiente forma:
npm install --save-dev enzyme-to-json
Esto es una de las mejores cosas, lo fácil que es configurar Enzyme en React. Simplemente abre el archivo src/setupTests.js
y lo dejaremos de la siguiente manera (si no tienes este archivo, créalo):
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import { createSerializer } from "enzyme-to-json";
Enzyme.configure({ adapter: new Adapter() });
expect.addSnapshotSerializer(createSerializer({ mode: "deep" }));
Nota: Si cuando leas este artículo React ya da soporte para la versión 17 no es necesario que hagas la configuración de este modo
import Enzyme from "enzyme";
import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
import { createSerializer } from "enzyme-to-json";
Enzyme.configure({ adapter: new Adapter() });
expect.addSnapshotSerializer(createSerializer({ mode: "deep" }));
¡Perfecto! 🎉 🚀 Ahora ya lo tenemos todo listo para empezar con nuestros tests.
Bien, para nuestro ejemplo vamos a utilizar el clásico ejemplo de un contador. Básicamente tendrá tres acciones:
Quedando así:
import React, { useState } from "react";
export const CounterApp = ({ value = 10 }) => {
const [counter, setCounter] = useState(value);
const handleUp = () => setCounter((counterPref) => counterPref + 1);
const handleDown = () => setCounter((counterPref) => counterPref - 1);
const handleReset = () => setCounter(value);
return (
<>
<h1>Counter App</h1>
<div>
<h2>{counter}</h2>
<div>
<button onClick={handleUp}>+1</button>
<button onClick={handleReset}>Reset</button>
<button onClick={handleDown}>-1</button>
</div>
</div>
</>
);
};
Y lo usamos de la siguiente manera:
<CounterApp value="{100}" />
Y visualmente quedaría algo como:
Bien, las pruebas que vamos a hacer serán las siguientes:
Primero pongo todo el test como va a quedar y lo iré explicando. Lo primero que debemos de crearnos es una carpeta donde iremos poniendo todos los tests, en mi caso, me he creado una carpeta tests
(en plural porque habrá más de uno) y dentro he colocado un CounterApp.test.js
. Es MUY IMPORTANTE poner en el nombre .test
porque sino React no se enterará de que eso es un test como tal. No lo olvides.
import "@testing-library/jest-dom";
import { shallow } from "enzyme";
import { CounterApp } from "../CounterApp";
describe("Probamos el componente <CounterApp />", () => {
let wrapper = shallow(<CounterApp />);
beforeEach(() => {
wrapper = shallow(<CounterApp />);
});
test("debería mostrar <CounterApp /> correctamente ", () => {
expect(wrapper).toMatchSnapshot();
});
test("debe de mostrar el valor por defecto de 100", () => {
const wrapper = shallow(<CounterApp value={100} />);
const counterText = wrapper.find("h2").text().trim();
expect(counterText).toBe("100");
});
test("debe incrementar con el botón +1", () => {
wrapper.find("button").at(0).simulate("click");
const counterText = wrapper.find("h2").text().trim();
expect(counterText).toBe("11");
});
test("debe decrementar con el botón -1", () => {
wrapper.find("button").at(2).simulate("click");
const counterText = wrapper.find("h2").text().trim();
expect(counterText).toBe("9");
});
test("debe de colocar el valor por defecto con el botón reset", () => {
const wrapper = shallow(<CounterApp value={105} />);
wrapper.find("button").at(0).simulate("click");
wrapper.find("button").at(1).simulate("click");
const counterText = wrapper.find("h2").text().trim();
expect(counterText).toBe("105");
});
});
Bien, vamos a explicar todo un poco.
import "@testing-library/jest-dom";
import { shallow } from "enzyme";
import { CounterApp } from "../CounterApp";
Esto no nos va a sorprender, ¿verdad?
@testing-library/jest-dom
dependencia no es obligatoria importarla, pero si recomendable porque así nos habilitará el IntelliSense para los tests, y esto es algo muy cómodo para no tener que ir recordando todos los nombres de las funciones y demás.enzyme
es la librería con el core de enzyme para los tests. Importación obligatoria.CounterApp
es nuestro componente con su ruta relativa que vamos a testear.´describe´ nos permite agrupar uno o varios tests para que de algún modo quede todo más legible y ordenado. En nuestro caso vamos a hacer un grupo (describe) con varias pruebas dentro de el mismo (test)
describe("Probamos el componente <CounterApp />", () => {
// Aquí dentro irán los tests para este grupo
});
let wrapper = shallow(<CounterApp />);
beforeEach(() => {
wrapper = shallow(<CounterApp />);
});
shallow
es una función de Enzyme que se utiliza para probar componentes de forma aislada, ya que no renderiza los subcomponentes. Si deseas renderizar los subcomponentes utiliza render o mount.
Así pues, con en wrapper
nos estamos "guardando" el componente para poder usarlo en las siguientes pruebas.
Con el beforeEach
lo que le decimos que el componente se reinicie al estado inicial cada vez que inicia una nueva prueba (esto lo explicaré luego).
Nota: Observarás que duplico la línea para definir el wrapper
con el shallow (shallow(<CounterApp />)
). No es lo más bonito la verdad, pero es la forma para mantener el IntelliSense a lo largo del archivo de tests. Si conoces una forma más limpia estoy abierto a todos los comentarios 🙃
Ahora entramos en materia buena. Lo primero que te recomiendo es pegarle un vistazo a la documentación sobre expect para que veas todas las cosas que podemos hacer con él.
test("debería mostrar <CounterApp /> correctamente ", () => {
expect(wrapper).toMatchSnapshot();
});
Con esto le decimos al test que esperamos que el componente se renderice correctamente y nos cree nuestro snapshot. ¿Qué es un snapshot? Pues básicamente una copia del html resultante que genera el componente. Verás que ahora tienes una nueva carpeta en src/tests/__snapshots__
con un archivo src/tests/__snapshots__/CounterApp.test.js.snap
que se verá así:
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Probamos el componente <CounterApp /> debería mostrar <CounterApp /> correctamente 1`] = `
<Fragment>
<h1>
Counter App
</h1>
<div>
<h2>
10
</h2>
<div>
<button
onClick={[Function]}
>
+1
</button>
<button
onClick={[Function]}
>
Reset
</button>
<button
onClick={[Function]}
>
-1
</button>
</div>
</div>
</Fragment>
`;
Si todo esto es correcto, perfecto, vamos bien 😌
test("debe de mostrar el valor por defecto de 100", () => {
const wrapper = shallow(<CounterApp value={100} />);
const counterText = wrapper.find("h2").text().trim();
expect(counterText).toBe("100");
});
Aquí instanciamos de nuevo el componente y se lo asignamos a una variable wrapper
, pero…. ¿porqué no usamos el wrapper que definimos al principio? Mira las diferencias:
const wrapper = shallow(<CounterApp value={100} />);
let wrapper = shallow(<CounterApp />);
En este nuevo caso necesitamos pasarle por props
el valos concreto que queremos, en este caso el 100 (por defecto el componente coge el valor 10, recuerda la definición del componente que era export const CounterApp = ({ value = 10 })
)
Lo siguiente, el counterText
es una variable en la que queremos guardar el texto que contiene la etiqueta h2 de nuestro componente. Si recordamos nuestro componente, tenemos:
<h2>{counter}</h2>
Entonces, con wrapper.find('h2').text().trim()
le decimos que busque la etiqueta <h2>
, obtenga su texto contenido y le aplica un trim por si acaso tiene espacios en blanco delante o detrás. Esto, como verás, es muy parecido al jQuery 🤨
Finalmente hacemos la comprobación: expect(counterText).toBe('100')
que básicamente es "preguntarle" a counterText si es === '100'.
test("debe incrementar con el botón +1", () => {
wrapper.find("button").at(0).simulate("click");
const counterText = wrapper.find("h2").text().trim();
expect(counterText).toBe("11");
});
Lo primero que debemos obtener es el botón +1. Recordemos nuestro componente:
<button onClick="{handleUp}">+1</button>
<button onClick="{handleReset}">Reset</button>
<button onClick="{handleDown}">-1</button>
Cuando hacemos wrapper.find('button')
obtenemos todos los botones de nuestro componente y nos los guarda en un array. Así pues, en la posición 0 estará el +1, en la posición 1 estará el reset y en la posición 2 estará el -1. Fácil, ¿no?
Entonces, capturamos el botón +1 y simulamos un click
de la siguiente forma: wrapper.find('button').at(0).simulate('click')
y buscamos de nuevo el valor que contiene la etiqueta h2 y lo verificamos: expect(counterText).toBe('11')
test("debe decrementar con el botón -1", () => {
wrapper.find("button").at(2).simulate("click");
const counterText = wrapper.find("h2").text().trim();
expect(counterText).toBe("9");
});
Entonces, capturamos el botón -1 y simulamos un click
de la siguiente forma: wrapper.find('button').at(2).simulate('click')
y buscamos de nuevo el valor que contiene la etiqueta h2 y lo verificamos: expect(counterText).toBe('9')
. Fácil.
test("debe de colocar el valor por defecto con el botón reset", () => {
const wrapper = shallow(<CounterApp value={105} />);
wrapper.find("button").at(0).simulate("click");
wrapper.find("button").at(1).simulate("click");
const counterText = wrapper.find("h2").text().trim();
expect(counterText).toBe("105");
});
Este test nos servirá para comprobar que el valor vuelve a ser el que le hemos pasado una vez le hemos sumado +1 y le pulsamos el botón reset. Del código de este test ya nos debería de sonar todo:
Primero, definimos un nuevo wrapper porque queremos pasarle un valor por defecto, para nuestro ejemplo será el 105. Luego pulsamos el botón de la posición 0 que es el de sumar +1 (ahora el valor en el componente será de 106).
Luego hacemos otro click, el del botón de la posición 1 que es el de Reset para que vuelva al valor pasado por props (105). Y obtenemos el valor de nuevo de la etiqueta h2. ¿Resultado? 105 😉
Si todo ha ido bien deberías de ver todos los checks verdes.
¡Es hora de celebrarlo! 🎉
Prueba a comentar la línea del beforeEach:
beforeEach(() => {
wrapper = shallow(<CounterApp />);
});
Y vuelve a correr los tests.
¿Que ha pasado? ¡Pero si no he tocado nada del código! La explicación es sencilla, y verás que tiene su lógica.
Todos los tests se ejecutan de forma secuencial. Como verás nos ha fallado el test al comprobar el valor cuando restamos -1. El test esperaba recibir el valor de 9, pero en cambio recibe un valor de 10. ¡¿WTF?! Recuerda, el valor por defecto es de 10, en la prueba debe incrementar con el botón +1
le hemos sumado +1 (11), y en la siguiente debe decrementar con el botón -1
le hemos restado -1 a ese valor 11 que teníamos de la prueba anterior, por tanto tenemos el 10. De ahí el error.
Entonces, con beforeEach, lo que hacemos es reiniciar el componente en cada test que queremos pasar y así nos aseguramos siempre del estado que queremos tener y esperamos para cada uno de ellos. 👏🏻
A todos nos gusta picar código desde un primer momento y nos olvidamos de los tests, yo incluido, bien por falta de tiempo del proyecto, o bien por pereza.
Pero para hacer tests no debemos volvernos tampoco locos. Testea con cabeza y no quieras hacer tests de cosas que no te sean necesarias. Verás que la curva de aprendizaje no es alta, y poco a poco le cogerás el punto y ganarás en salud, sobre todo en eso 😂
¡Haz tests! 🙏🏻
Repo: https://github.com/alextomas80/testing-components-enzyme
Y esto es todo. Espero que te pueda servir 🙃