Hacer test de componentes en React con Enzyme
Tags: react, enzyme, testing, javascript
Published: 2021-02-11 20:15:55

>Post original: https://alextomas.com/blog/primeros-pasos-test-enzyme

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](https://enzymejs.github.io/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**:

-   Instalación de Enzyme en React
    -   React 16
    -   React 17
-   Snapshots
-   Configuración de Enzyme en React
    -   Configuración de Enzyme para React 16
    -   Configuración de Enzyme para React 17
-   Componente de ejemplo para los tests: <CounterApp />
-   Tests que vamos a realizar sobre el componente
-   Testing
    -   Importamos las dependencias
    -   ¿Qué es describe?
    -   Los tests, ¡por fin!
        -   Test 1: debería mostrar <CounterApp /> correctamente
        -   Test 2: debe de mostrar el valor por defecto de 100
        -   Test 3: debe incrementar con el botón +1
        -   Test 4: debe decrementar con el botón -1
        -   Test 5: debe de colocar el valor por defecto con el botón reset
        -   Resultado final de los tests
        -   beforeEach
-   Conclusiones

## Instalación de Enzyme en React

Lo primero que debemos de mirar es la [documentación de Enzyme para la instalación](https://enzymejs.github.io/enzyme/#installation), y aquí haremos una matización.

### React 16

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:

![Versión de React en tu proyecto](https://alextomas.com/blog/testing-enzyme/version-react.jpg)

Si este es tu caso la instalación sería de la siguiente forma:

```bash
npm i --save-dev enzyme enzyme-adapter-react-16
```

### React 17

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:

```bash
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.

```bash
npm install --save-dev @wojtekmaj/enzyme-adapter-react-17
```

## Snapshots

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:

```bash
npm install --save-dev enzyme-to-json
```

## Configuración de Enzyme en React

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):

### Configuración de Enzyme para React 16

```javascript
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' }));
```

### Configuración de Enzyme para React 17

_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

```javascript
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.

## Componente de ejemplo para los tests: <CounterApp />

Bien, para nuestro ejemplo vamos a utilizar el clásico ejemplo de un contador. Básicamente tendrá tres acciones:

1. Botón para aumentar +1 el contador
2. Botón para resetear el contador
3. Botón para restar -1 el contador

Quedando así:

```javascript
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:

```html
<CounterApp value="{100}" />
```

Y visualmente quedaría algo como:

![Versión de React en tu proyecto](https://alextomas.com/blog/testing-enzyme/vista-previa-counter-app.jpg)

## Tests que vamos a realizar sobre el componente

Bien, las pruebas que vamos a hacer serán las siguientes:

1. Se debería mostrar <CounterApp /> correctamente.
2. Debe de mostrar el valor por defecto de 100
3. Debe incrementar con el botón +1
4. Debe decrementar con el botón -1
5. Debe de colocar el valor por defecto con el botón reset

## Testing

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.

```javascript
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.

### Importamos las dependencias

```javascript
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.

### ¿Qué es describe?

´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)

```javascript
describe('Probamos el componente <CounterApp />', () => {
	// Aquí dentro irán los tests para este grupo
});
```

### Los tests, ¡por fin!

```javascript
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 🙃

#### Test 1: debería mostrar <CounterApp /> correctamente

Ahora entramos en materia buena. Lo primero que te recomiendo es pegarle un vistazo a la documentación sobre [expect](https://jestjs.io/docs/en/expect) para que veas todas las cosas que podemos hacer con él.

```javascript
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í:

```javascript
// 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 2: debe de mostrar el valor por defecto de 100

```javascript
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:

```javascript
const wrapper = shallow(<CounterApp value={100} />);
```

```javascript
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:

```html
<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 3: debe incrementar con el botón +1

```javascript
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:

```html
<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 4: debe decrementar con el botón -1

```javascript
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 5: debe de colocar el valor por defecto con el botón reset

```javascript
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 😉

#### Resultado final de los tests

Si todo ha ido bien deberías de ver todos los checks verdes.
![Checks verdes, test en Enzyme pasados correctamente](https://alextomas.com/blog/testing-enzyme/resultado-ok-tests.jpg)

¡Es hora de celebrarlo! 🎉

![Testing success](https://alextomas.com/gifs/success.gif)

#### beforeEach

Prueba a comentar la línea del beforeEach:

```javascript
beforeEach(() => {
	wrapper = shallow(<CounterApp />);
});
```

Y vuelve a correr los tests.

![Checks rojos, test en Enzyme con errores](https://alextomas.com/blog/testing-enzyme/resultado-ko-tests.jpg)

¿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. 👏🏻

## Conclusiones

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 😉

[Alex Tomás](https://alextomas.com)