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