# Testing de componentes en React
## Instalación de dependencias
Para testear componentes en React necesitaremos un entorno en base a Jest y React Testing Library. Además de tener evidentemente un proyecto en React funcionando, con todas sus dependencias.
```bash!
$npm install --save-dev jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom
```
Además debemos configurar Jest para que sea capaz de utilizar jsdom para hacer un render fake de nuestros componentes.
*jest.config.js*
```javascript!
const nextJest = require("next/jest")
const createJestConfig = nextJest({
dir: "./src",
})
const customJestConfig = {
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
moduleDirectories: ["node_modules", "<rootDir>/"],
testEnvironment: "jest-environment-jsdom",
// En caso de utilizar alias
moduleNameMapper: {
"@components/(.*)": "<rootDir>/src/components/$1",
"@containers/(.*)": "<rootDir>/src/containers/$1",
"@pages/(.*)": "<rootDir>/src/pages/$1",
},
}
module.exports = createJestConfig(customJestConfig)
```
*jest.setup.js*
```javascript!
// Optional: configure or set up a testing framework before each test.
// If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js`
// Used for __tests__/testing-library.js
// Learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom/extend-expect"
```
## Testing Library
Esta librería está hecha de tal forma que se simula el comportamiento del usuario final con los componentes. Como usuario no vamos a saber como es el árbol DOM que hay por debajo, interectuaremos a través de elementos visuales que nos sirvan de referencia.
A la hora de desarrollar los tests es importante que adoptemos ese enfoque, y hacer tests basados en casos de uso y no en el código.
### Orden de Prioridad
Testing library en su documentación nos anota una serie de recomendaciones sobre prioridades a la hora de seleccionar elementos.
[Prioridades Testing Library](https://testing-library.com/docs/queries/about/#priority)
### Funciones interesantes
- **toHaveTextContent**: Busca el texto que le pasamos por parámetro.
```javascript!
/*...*/
expect(container).toHaveTextContent(title)
/*...*/
```
- **toBeInDocument**: Busca el elemento en el DOM simulado.
```javascript!
/*...*/
expect(heading).toBeInTheDocument()
/*...*/
```
- **toMatchSnapShot**: Busca el contenedor o elemento que pasemos en el SnapShot que crea Jest.
```javascript!
/*...*/
expect(container).toMatchSnapshot()
/*...*/
```
- **component.getByText**: Busca un elemento por el texto que pasemos por parámetros.
```javascript!
/*...*/
const registerBtn = component.getByText("Crear cuenta")
/*...*/
```
- **component.debug**: Te permite visualizar lo que se está renderizando cuándo ejecutas los tests.
```jsx!
const component = render(<Component />)
component.debug()
```
- **prettyDOM**: Te permite ver de una forma más bonita el componente o elemento que le pasemos por parámetro.
```javascript!
const component = render(<Component />)
const button = component.getByText("hello")
console.log(prettyDOM(button))
```
- **element.click**: Ejecuta el evento onClick en el elemento.
```javascript!
/*...*/
const registerBtn = component.getByText("Crear cuenta")
registerBtn.click()
/*...*/
```
- Cuando busques elementos por text, label, etc. Es recomendable usar el siguiente tipo de expresión regular, pues en caso de que pasemos alguna mayúscula / minúscula nos seguirá funcionando.
```javascript=
/*...*/
const registerBtn = component.getByText(/Crear cuenta/i)
/*...*/
```
- Podemos hacer uso de la función within, esta se utiliza para seleccionar un elemento del DOM dentro de un contenedor específico.
```javascript=
/*...*/
const elementoPadre = document.querySelector(".padre")
// Buscamos aquellas coincidencias que existan dentro del contenedor padre
const elementoHijo = within(elementoPadre).getByText('hijo')
/*...*/
```
- En caso de querer especifcar que algo "no sea", debemos utilizar el modificador .not
```javascript=
expect(screen.queryByText(/Hola/i).not.toBeInTheDocument())
```
- Con *render* podemos renderizar un componente dentro del test y probarlo dentro de este.
```javascript=
const component = render(<Component />)
```
- **findBy()**: test asíncronos. Se utiliza cuando esperamos que aparezca un elemento en el DOM, pero que igual esto no ocurre al momento.
```javascript=
fireEvent.click(button)
await screen.findByText('He sido clicado')
```
- **waitFor()**: test asíncrono, Espera a que se cumpla la condición que le hemos establecido antes de continuar.
```javascript=
await waitForElementToBeRemoved(() =>
screen.queryByText(/Me voy a borrar/i)
);
```
- **toHaveBeenCalled()**: se utiliza para comprobar que se ha llamado a una función en concreto.
```javascript=
/*...*/
funcionEjemplo()
expect(funcionEjemplo).toHaveBeenCalled()
/*...*/
```
- **toHaveReturned()**: para comprobar que una función se ha ejecutado correctamente, es decir, no devuelve un error.
```javascript=
/*...*/
const funcionEjemplo = () => true
funcionEjemplo()
expect(funcionEjemplo).toHaveReturned()
/*...*/
```
- **toHaveLength()**: para comprobar que un objeto tiene una longitud concreta.
```javascript=
/*...*/
expect([1, 2, 3]).toHaveLength(3)
/*...*/
```
- **fireEvent**: dispara ciertos eventos dentro del DOM.
- **fireEvent.change()**: se usa cuando esperamos que un elemento cambie su valor.
```javascript=
/*...*/
const inputEjemplo = getByLabelText(/Valor inicial/i);
fireEvent.change(inputEjemplo, { target: { value: 'Nuevo valor' } });
expect(inputEjemplo.value).toBe('Nuevo valor');
/*...*/
```
- **fireEvent.drop()**: se usa para simular eventos en los que hay que arrastrar y soltar elementos.
```javascript=
test('MyComponent actualiza el orden de los elementos al soltar', () => {
const { getByTestId } = render(<MyComponent />);
const firstItem = getByTestId('item-1');
const secondItem = getByTestId('item-2');
fireEvent.dragStart(firstItem);
fireEvent.drop(secondItem);
expect(getByTestId('item-1')).toBe(secondItem);
expect(getByTestId('item-2')).toBe(firstItem);
});
```
Debemos tener en cuenta que fireEvent.drop se debe usar con *fireEvent.dragStart* y *fireEvent.dragOver* para simular todo el proceso de arrastrar y soltar.
## Cheat Sheet
Existen tres tipos de búsquedas:
getBy / getAllBy: Busca de forma síncrona, Si no lo encuentra lanza una excepción
queryBy / queryAllBy: Busca de forma síncrona, Si no lo encuentra devuelve null
findBy / findAllBy: Busca los elementos de forma asíncrona. Si no lo encuentra lanza una excepción
--------------------------------
Una vez elegido el método de búsqueda, debemos usar un selector. Estos selectores vienen dados por testing library con un orden de prioridad en cuanto a cual aporta mayor accesibilidad a la web:
LabelText
Placeholder
Text
DisplayValue
AltText
Title
Role
TestId
Estos se combinan para crear la función:
Ej: getByText()
[Cheat sheet Test](https://testing-library.com/docs/react-testing-library/cheatsheet/)
## Enlaces útiles
[Testing playground](https://testing-playground.com/)
## Dudas
1. ¿Es bueno seleccionar un elemento por id o cualquier otro selector de CSS?
Es mejor basarse en otros aspectos, ya que si nos basamos en selectores CSS estamos acoplando nuestros tests a el html, y si en el futuro cambia un id, el tipo de elemento o lo que sea, se romperan los test que depende de ello.
Lo suyo es que cuando te basas en roles, haces los test mas resilientes a cambios, porque no ponen el foco en html, sino en lo que buscaria un usuario visualmente (rol=button, text=submit).
Aparte que indirectamente se mejora la accesibilidad.
### Out of Context
1. ¿Mejor uso de enums o interfaces para valores constantes?
## Vídeos de Apoyo
[Live Coding Componentes en React con TDD by *Adrián Ferrera*](https://youtu.be/iYhwoludFvg)
[Testing en React by *midudev*](https://youtu.be/KYjjtRgg_H0)