Cómo probar una aplicación React con Jest y la biblioteca de pruebas React

El autor seleccionó a Vets Who Code para recibir una donación como parte del programa Write for DOnations .
Introducción
Obtener una cobertura de pruebas sólida es fundamental para generar confianza en su aplicación web. Jest es un ejecutor de pruebas de JavaScript que proporciona recursos para escribir y ejecutar pruebas. React Testing Library ofrece un conjunto de ayudantes de pruebas que estructuran sus pruebas en función de las interacciones del usuario en lugar de los detalles de implementación de los componentes. Tanto Jest como React Testing Library vienen preempaquetados con Create React App y se adhieren al principio rector de que las aplicaciones de prueba deben parecerse a cómo se utilizará el software.
En este tutorial, probará código asincrónico e interacciones en un proyecto de muestra que contiene varios elementos de la interfaz de usuario. Utilizará Jest para escribir y ejecutar pruebas unitarias e implementará React Testing Library como una biblioteca auxiliar DOM ( Document Object Model ) para manejar la interacción con los componentes.
Prerrequisitos
Para completar este tutorial, necesitarás:
-
La versión 14 o posterior de Node.js está instalada en tu máquina local. Para instalar Node.js en macOS o Ubuntu 18.04, sigue los pasos que se indican en Cómo instalar Node.js y crear un entorno de desarrollo local en macOS o en la sección Instalación mediante un PPA de Cómo instalar Node.js en Ubuntu 18.04 .
-
npm
Versión 5.2 o superior en tu máquina local, que necesitarás para usar Create React App ynpx
en el proyecto de muestra. Si no lo instalastenpm
junto conNode.js
, hazlo ahora. Para Linux, usa el comandosudo apt install npm
.- Para
npm
que los paquetes funcionen en este tutorial, instale elbuild-essential
paquete. Para Linux, utilice el comandosudo apt install build-essential
.
- Para
-
Git está instalado en tu máquina local. Puedes comprobar si Git está instalado en tu computadora o realizar el proceso de instalación para tu sistema operativo con Cómo instalar Git en Ubuntu 20.04 .
-
Familiaridad con React, que puede desarrollar con la serie Cómo codificar en React.js . Debido a que el proyecto de muestra se inicia con Create React App , no necesita instalarlo por separado.
-
Es útil tener cierta familiaridad con Jest como ejecutor de pruebas o marco de trabajo, pero no es obligatorio. Dado que Jest viene preempaquetado con Create React App, no es necesario instalarlo por separado.
Paso 1: Configuración del proyecto
En este paso, clonarás un proyecto de muestra y lanzarás el conjunto de pruebas. El proyecto de muestra utiliza tres herramientas principales: Create React App, Jest y React Testing Library. Create React App se utiliza para iniciar una aplicación React de una sola página. Jest se utiliza como ejecutor de pruebas y React Testing Library proporciona ayudantes de pruebas para estructurar las pruebas en torno a las interacciones del usuario.
Para comenzar, clonarás una aplicación React preconstruida desde GitHub. Trabajarás con la aplicación Doggy Directory , que es un proyecto de muestra que aprovecha la API Dog para crear un sistema de búsqueda y visualización para una colección de imágenes de perros según una raza específica.
Para clonar el proyecto desde Github, abra su terminal y ejecute el siguiente comando:
- git clone https://github.com/do-community/doggy-directory
Verá un resultado similar a este:
OutputCloning into 'doggy-directory'...remote: Enumerating objects: 64, done.remote: Counting objects: 100% (64/64), done.remote: Compressing objects: 100% (48/48), done.remote: Total 64 (delta 21), reused 55 (delta 15), pack-reused 0Unpacking objects: 100% (64/64), 228.16 KiB | 3.51 MiB/s, done.
Cambiar a la doggy-directory
carpeta:
- cd doggy-directory
Instalar las dependencias del proyecto:
- npm install
El npm install
comando instalará todas las dependencias del proyecto definidas en el package.json
archivo.
Después de instalar las dependencias, puede ver la versión implementada de la aplicación o puede ejecutar la aplicación localmente con el siguiente comando:
- npm start
Si elige ejecutar la aplicación localmente, se abrirá en http://localhost:3000/
. Verá el siguiente resultado en la terminal:
OutputCompiled successfully!You can now view doggy-directory in the browser.Local: http://localhost:3000On Your Network: http://network_address:3000
Después del lanzamiento, la página de destino de la aplicación se verá así:
Se han instalado las dependencias del proyecto y la aplicación ya está en ejecución. A continuación, abra una nueva terminal y ejecute las pruebas con el siguiente comando:
- npm test
El npm test
comando inicia las pruebas en un modo de observación interactivo con Jest como ejecutor de pruebas. Cuando está en modo de observación, las pruebas se vuelven a ejecutar automáticamente después de que se modifica un archivo. Las pruebas se ejecutarán siempre que modifique un archivo y le informarán si ese cambio pasó las pruebas.
Después de ejecutarlo npm test
por primera vez, verá este resultado en la terminal:
OutputNo tests found related to files changed since last commit.Press `a` to run all tests, or run Jest with `--watchAll`.Watch Usage › Press a to run all tests. › Press f to run only failed tests. › Press q to quit watch mode. › Press p to filter by a filename regex pattern. › Press t to filter by a test name regex pattern. › Press Enter to trigger a test run.
Ahora que tiene la aplicación de ejemplo y el conjunto de pruebas en ejecución, puede comenzar a probar con la página de destino.
Paso 2: Prueba de la página de destino
De forma predeterminada, Jest buscará archivos con el .test.js
sufijo y archivos con el .js
sufijo en __tests__
carpetas. Cuando realice cambios en los archivos de prueba relevantes, se detectarán automáticamente. A medida que se modifiquen los casos de prueba, la salida se actualizará automáticamente. El archivo de prueba preparado para el doggy-directory
proyecto de muestra se configura con un código mínimo antes de agregar paradigmas de prueba. En este paso, escribirá pruebas para verificar que la página de destino de la aplicación se cargue antes de realizar una búsqueda.
Ábrelo src/App.test.js
en tu editor para ver el siguiente código:
fuente/App.test.js
import { render, screen } from '@testing-library/react';import App from './App';test('renders the landing page', () = { render(App /);});
Se requiere un mínimo de un bloque de prueba en cada archivo de prueba. Cada bloque de prueba acepta dos parámetros obligatorios: el primer argumento es una cadena que representa el nombre del caso de prueba; el segundo argumento es una función que contiene las expectativas de la prueba.
Dentro de la función, hay un render
método que proporciona la biblioteca de pruebas de React para representar el componente en el DOM. Una vez que el componente que desea probar se represente en el DOM del entorno de prueba, puede comenzar a escribir código para confirmar la funcionalidad esperada.
Agregará un bloque de prueba al render
método que comprobará si la página de destino se muestra correctamente antes de realizar cualquier llamada o selección a la API. Agregue el código resaltado debajo del render
método:
fuente/App.test.js
...test('renders the landing page', () = { render(App /); expect(screen.getByRole("heading")).toHaveTextContent(/Doggy Directory/); expect(screen.getByRole("combobox")).toHaveDisplayValue("Select a breed"); expect(screen.getByRole("button", { name: "Search" })).toBeDisabled(); expect(screen.getByRole("img")).toBeInTheDocument();});
La expect
función se utiliza cada vez que se desea verificar un determinado resultado y acepta un único argumento que representa el valor que produce el código. La mayoría de expect
las funciones se combinan con una función de comparación para afirmar algo sobre un valor en particular. Para la mayoría de estas afirmaciones, se utilizarán comparadores adicionales proporcionados por jest-dom para facilitar la comprobación de aspectos comunes encontrados en el DOM. Por ejemplo, .toHaveTextContent
es el comparador para la expect
función en la primera línea, mientras que getByRole("heading")
es el selector para capturar el elemento del DOM.
React Testing Library proporciona el screen
objeto como una forma conveniente de acceder a las consultas pertinentes necesarias para realizar afirmaciones en el entorno DOM de prueba. De forma predeterminada, React Testing Library proporciona consultas que permiten ubicar elementos dentro del DOM. Hay tres categorías principales de consultas:
getBy*
(más comúnmente usado)queryBy*
(se utiliza para probar la ausencia de un elemento sin generar un error)findBy*
(se utiliza al probar código asincrónico)
Cada tipo de consulta cumple una función específica que se definirá más adelante en el tutorial. En este paso, te centrarás en la consulta, que es el tipo de consulta más común. Para ver una lista exhaustiva de las diferentes variaciones de consulta, puedes revisar la hoja de referencia de consultasgetBy*
de React .
A continuación se muestra una imagen anotada de la página de inicio de Doggy Directory que indica cada sección que cubre la primera prueba (al representar la página de inicio):
Cada expect
función se afirma frente a lo siguiente (que se muestra en la imagen anotada de arriba):
- Se espera que el elemento con el rol de encabezado tenga una coincidencia de subcadena de Doggy Directory .
- Se espera que la entrada de selección tenga un valor de visualización exacto de Seleccionar una raza .
- Se espera que el botón Buscar esté deshabilitado ya que no se ha realizado ninguna selección.
- Se espera que la imagen de marcador de posición esté presente en el documento ya que no se ha realizado ninguna búsqueda.
Cuando haya terminado, guarde el src/App.test.js
archivo. Dado que las pruebas se ejecutan en modo de observación, los cambios se registrarán automáticamente. Si los cambios no se registran automáticamente, es posible que deba detener y reiniciar el conjunto de pruebas.
Ahora, cuando mires tus pruebas en la terminal, verás el siguiente resultado:
Output PASS src/App.test.js ✓ renders the landing page (172 ms)Test Suites: 1 passed, 1 totalTests: 1 passed, 1 totalSnapshots: 0 totalTime: 2.595 s, estimated 5 sRan all test suites related to changed files.Watch Usage: Press w to show more.
En este paso, escribiste una prueba inicial para verificar la vista de renderizado inicial de la página de inicio de Doggy Directory. En el siguiente paso, aprenderás a simular una llamada de API para probar código asincrónico.
Paso 3: burlarse del fetchmétodo
En este paso, revisará un enfoque para simular fetch
el método de JavaScript. Si bien existen numerosas formas de lograrlo, esta implementación utilizará los métodos spyOn
y de Jest mockImplementation
.
Cuando se depende de API externas, existe la posibilidad de que su API deje de funcionar o tarde un tiempo en devolver una respuesta. Simulando el fetch
método se proporciona un entorno consistente y predecible, lo que le da más confianza en sus pruebas. Un mecanismo de simulación de API es necesario para ejecutar correctamente las pruebas que utilizan una API externa.
Nota: En un esfuerzo por simplificar este proyecto, simulará el método de búsqueda. Sin embargo, se recomienda utilizar una solución más sólida como Mock Service Worker (MSW) al simular código asincrónico para bases de código más grandes y listas para producción.
Ábrelo src/mocks/mockFetch.js
en tu editor para revisar cómo mockFetch
funciona el método:
fuente/mocks/mockFetch.js
const breedsListResponse = { message: { boxer: [], cattledog: [], dalmatian: [], husky: [], },};const dogImagesResponse = { message: [ "https://images.dog.ceo/breeds/cattledog-australian/IMG_1042.jpg ", "https://images.dog.ceo/breeds/cattledog-australian/IMG_5177.jpg", ],};export default async function mockFetch(url) { switch (url) { case "https://dog.ceo/api/breeds/list/all": { return { ok: true, status: 200, json: async () = breedsListResponse, }; } case "https://dog.ceo/api/breed/husky/images" : case "https://dog.ceo/api/breed/cattledog/images": { return { ok: true, status: 200, json: async () = dogImagesResponse, }; } default: { throw new Error(`Unhandled request: ${url}`); } }}
El mockFetch
método devuelve un objeto que se parece mucho a la estructura de lo que fetch
devolvería una llamada en respuesta a las llamadas API dentro de la aplicación. El mockFetch
método es necesario para probar la funcionalidad asincrónica en dos áreas de la aplicación Doggy Directory: el menú desplegable de selección que completa la lista de razas y la llamada API para recuperar imágenes de perros cuando se realiza una búsqueda.
Cerrar src/mocks/mockFetch.js
. Ahora que comprende cómo mockFetch
se utilizará el método en sus pruebas, puede importarlo a su archivo de prueba. La mockFetch
función se pasará como argumento al mockImplementation
método y luego se utilizará como una implementación falsa de la API de búsqueda.
En src/App.test.js
, agregue las líneas de código resaltadas para importar el mockFetch
método:
fuente/App.test.js
import { render, screen } from '@testing-library/react';import mockFetch from "./mocks/mockFetch";import App from './App';beforeEach(() = { jest.spyOn(window, "fetch").mockImplementation(mockFetch);})afterEach(() = { jest.restoreAllMocks()});...
Este código configurará y desmantelará la implementación simulada para que cada prueba comience desde un campo de juego nivelado.
jest.spyOn(window, "fetch");
crea una función simulada que rastreará las llamadas al fetch
método adjunto a la variable de ventana global en el DOM.
.mockImplementation(mockFetch);
acepta una función que se utilizará para implementar el método simulado. Debido a que este comando anula la fetch
implementación original, se ejecutará siempre que fetch
se lo llame dentro del código de la aplicación.
Cuando termine, guarde el src/App.test.js
archivo.
Ahora, cuando mires tus pruebas en la terminal, recibirás el siguiente resultado:
Output console.error Warning: An update to App inside a test was not wrapped in act(...). When testing, code that causes React state updates should be wrapped into act(...): act(() = { /* fire events that update state */ }); /* assert on the output */ This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act at App (/home/sammy/doggy-directory/src/App.js:5:31) 18 | }) 19 | .then((json) = { 20 | setBreeds(Object.keys(json.message)); | ^ 21 | }); 22 | }, []); 23 | ... PASS src/App.test.js ✓ renders the landing page (429 ms)Test Suites: 1 passed, 1 totalTests: 1 passed, 1 totalSnapshots: 0 totalTime: 1.178 s, estimated 2 sRan all test suites related to changed files.
La advertencia le indica que se produjo una actualización de estado cuando no se esperaba. Sin embargo, el resultado también indica que las pruebas simularon el fetch
método correctamente.
En este paso, simulaste el fetch
método y lo incorporaste a un conjunto de pruebas. Aunque la prueba está pasando, aún debes abordar la advertencia.
Paso 4: Solución de la actadvertencia
En este paso, aprenderá cómo solucionar la act
advertencia que apareció después de los cambios en el Paso 3.
La act
advertencia se produce porque ha simulado el fetch
método y, cuando se monta el componente, realiza una llamada a la API para obtener la lista de razas. La lista de razas se almacena en una variable de estado que completa el option
elemento dentro de la entrada de selección.
La siguiente imagen muestra cómo se ve la entrada de selección después de realizar una llamada API exitosa para completar la lista de razas:
La advertencia se lanza porque el estado se establece después de que el bloque de prueba termina de representar el componente.
Para solucionar este problema, agregue las modificaciones resaltadas al caso de prueba en src/App.test.js
:
fuente/App.test.js
...test('renders the landing page', async () = { render(App /); expect(screen.getByRole("heading")).toHaveTextContent(/Doggy Directory/); expect(screen.getByRole("combobox")).toHaveDisplayValue("Select a breed"); expect(await screen.findByRole("option", { name: "husky"})).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Search" })).toBeDisabled(); expect(screen.getByRole("img")).toBeInTheDocument();});
La async
palabra clave le dice a Jest que el código asincrónico se ejecuta como resultado de la llamada API que ocurre cuando se monta el componente.
Una nueva afirmación con la findBy
consulta verifica que el documento contiene una opción con el valor husky
. findBy
Las consultas se utilizan cuando se necesita probar código asincrónico que depende de que algo esté en el DOM después de un período de tiempo. Debido a que la findBy
consulta devuelve una promesa que se resuelve cuando el elemento solicitado se encuentra en el DOM, la await
palabra clave se utiliza dentro del expect
método.
Cuando haya terminado, guarde los cambios realizados en src/App.test.js
.
Con las nuevas incorporaciones, ahora verás que la act
advertencia ya no está presente en tus pruebas:
Output PASS src/App.test.js ✓ renders the landing page (123 ms)Test Suites: 1 passed, 1 totalTests: 1 passed, 1 totalSnapshots: 0 totalTime: 0.942 s, estimated 2 sRan all test suites related to changed files.Watch Usage: Press w to show more.
En este paso, aprendiste a solucionar la act
advertencia que puede aparecer al trabajar con código asincrónico. A continuación, agregarás un segundo caso de prueba para verificar las funcionalidades interactivas de la aplicación Doggy Directory.
Paso 5: Prueba de la función de búsqueda
En el paso final, escribirá un nuevo caso de prueba para verificar la función de búsqueda y visualización de imágenes. Aprovechará una variedad de consultas y métodos API para lograr la cobertura de prueba adecuada.
Regrese al src/App.test.js
archivo en su editor. En la parte superior del archivo, importe la user-event
biblioteca complementaria y el waitForElementToBeRemoved
método async en el archivo de prueba con los comandos resaltados:
fuente/App.test.js
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react';import userEvent from '@testing-library/user-event'; ...
Utilizarás estas importaciones más adelante en esta sección.
Después del método inicial test()
, agregue un nuevo bloque de prueba asíncrono y represente el App
componente con el siguiente bloque de código:
fuente/App.test.js
...test("should be able to search and display dog image results", async () = { render(App /);})
Con el componente renderizado, ahora puedes agregar funciones que verifiquen las características interactivas de la aplicación Doggy Directory.
Aún en src/App.test.js
, agregue los bloques de código resaltados dentro del segundo test()
método:
fuente/App.test.js
...test("should be able to search and display dog image results", async () = { render(App /); //Simulate selecting an option and verifying its value const select = screen.getByRole("combobox"); expect(await screen.findByRole("option", { name: "cattledog"})).toBeInTheDocument(); userEvent.selectOptions(select, "cattledog"); expect(select).toHaveValue("cattledog");})
La sección resaltada arriba simulará la selección de una raza de perro y verificará que se muestre el valor correcto.
La getByRole
consulta toma el elemento seleccionado y lo asigna a la select
variable.
De manera similar a cómo solucionó la act
advertencia en el Paso 4, use la findByRole
consulta para esperar a que la cattledog
opción aparezca en el documento antes de continuar con más afirmaciones.
El userEvent
objeto importado anteriormente simulará interacciones de usuario habituales. En este ejemplo, el selectOptions
método selecciona la cattledog
opción que esperaba en la línea anterior.
La última línea afirma que la select
variable contiene el cattledog
valor seleccionado anteriormente.
La siguiente sección que agregará al test()
bloque Javascript iniciará la solicitud de búsqueda para encontrar imágenes de perros según la raza seleccionada y confirmará la presencia de un estado de carga.
Añade las líneas resaltadas:
fuente/App.test.js
...test("should be able to search and display dog image results", async () = { render(App /); //...Simulate selecting an option and verifying its value //Simulate initiating the search request const searchBtn = screen.getByRole("button", { name: "Search" }); expect(searchBtn).not.toBeDisabled(); userEvent.click(searchBtn); //Loading state displays and gets removed once results are displayed await waitForElementToBeRemoved(() = screen.queryByText(/Loading/i));})
La getByRole
consulta localiza el botón de búsqueda y lo asigna a la searchBtn
variable.
El toBeDisabled
comparador jest-dom verificará que el botón de búsqueda no esté deshabilitado cuando se realiza una selección de raza.
El click
método del userEvent
objeto simula hacer clic en el botón de búsqueda.
La función waitForElementToBeRemoved
auxiliar asíncrona importada anteriormente esperará la aparición y desaparición del mensaje de carga mientras la llamada a la API de búsqueda está en curso. queryByText
Dentro de la waitForElementToBeRemoved
devolución de llamada, verifica la ausencia de un elemento sin generar un error.
La siguiente imagen muestra el estado de carga que se mostrará cuando una búsqueda esté en curso:
A continuación, agregue el siguiente código Javascript para validar la visualización de la imagen y el recuento de resultados:
fuente/App.test.js
...test("should be able to search and display dog image results", async () = { render(App /) //...Simulate selecting an option and verifying its value //...Simulate initiating the search request //...Loading state displays and gets removed once results are displayed //Verify image display and results count const dogImages = screen.getAllByRole("img"); expect(dogImages).toHaveLength(2); expect(screen.getByText(/2 Results/i)).toBeInTheDocument(); expect(dogImages[0]).toHaveAccessibleName("cattledog 1 of 2"); expect(dogImages[1]).toHaveAccessibleName("cattledog 2 of 2");})
La getAllByRole
consulta seleccionará todas las imágenes de perros y las asignará a la dogImages
variable. La *AllBy*
variante de la consulta devuelve una matriz que contiene varios elementos que coinciden con el rol especificado. La *AllBy*
variante se diferencia de la ByRole
variante, que solo puede devolver un único elemento.
La fetch
implementación simulada contenía dos URL de imágenes dentro de la respuesta. Con toHaveLength
el comparador de Jest, puedes verificar que se muestran dos imágenes.
La getByText
consulta verificará que el recuento de resultados adecuado aparezca en la esquina derecha.
Dos afirmaciones que utilizan los toHaveAccessibleName
comparadores verifican que el texto alternativo apropiado esté asociado con imágenes individuales.
Una búsqueda completa que muestre imágenes del perro según la raza seleccionada junto con la cantidad de resultados encontrados se verá así:
Cuando combinas todas las piezas del nuevo código Javascript, el App.test.js
archivo se verá así:
fuente/App.test.js
import {render, screen, waitForElementToBeRemoved} from '@testing-library/react';import userEvent from '@testing-library/user-event';import mockFetch from "./mocks/mockFetch";import App from './App';beforeEach(() = { jest.spyOn(window, "fetch").mockImplementation(mockFetch);})afterEach(() = { jest.restoreAllMocks();});test('renders the landing page', async () = { render(App /); expect(screen.getByRole("heading")).toHaveTextContent(/Doggy Directory/); expect(screen.getByRole("combobox")).toHaveDisplayValue("Select a breed"); expect(await screen.findByRole("option", { name: "husky"})).toBeInTheDocument() expect(screen.getByRole("button", { name: "Search" })).toBeDisabled(); expect(screen.getByRole("img")).toBeInTheDocument();});test("should be able to search and display dog image results", async () = { render(App /); //Simulate selecting an option and verifying its value const select = screen.getByRole("combobox"); expect(await screen.findByRole("option", { name: "cattledog"})).toBeInTheDocument(); userEvent.selectOptions(select, "cattledog"); expect(select).toHaveValue("cattledog"); //Initiate the search request const searchBtn = screen.getByRole("button", { name: "Search" }); expect(searchBtn).not.toBeDisabled(); userEvent.click(searchBtn); //Loading state displays and gets removed once results are displayed await waitForElementToBeRemoved(() = screen.queryByText(/Loading/i)); //Verify image display and results count const dogImages = screen.getAllByRole("img"); expect(dogImages).toHaveLength(2); expect(screen.getByText(/2 Results/i)).toBeInTheDocument(); expect(dogImages[0]).toHaveAccessibleName("cattledog 1 of 2"); expect(dogImages[1]).toHaveAccessibleName("cattledog 2 of 2");})
Guarde los cambios realizados en src/App.test.js
.
Cuando revise sus pruebas, la salida final en la terminal ahora tendrá la siguiente salida:
Output PASS src/App.test.js ✓ renders the landing page (273 ms) ✓ should be able to search and display dog image results (123 ms)Test Suites: 1 passed, 1 totalTests: 2 passed, 2 totalSnapshots: 0 totalTime: 4.916 sRan all test suites related to changed files.Watch Usage: Press w to show more.
En este último paso, agregó una prueba que verifica las funciones de búsqueda, carga y visualización de la aplicación Doggy Directory. Con la afirmación final escrita, ahora sabe que su aplicación funciona.
Conclusión
A lo largo de este tutorial, escribiste casos de prueba usando Jest, React Testing Library y los comparadores jest-dom. Al crear de forma incremental, escribiste pruebas basadas en cómo un usuario interactúa con la IU. También aprendiste las diferencias entre las consultas getBy*
, findBy*
y queryBy*
, y cómo probar código asincrónico.
Para obtener más información sobre los temas mencionados anteriormente, consulta la documentación oficial de Jest , React Testing Library y jest-dom . También puedes leer Common Mistakes with React Testing Library de Kent C. Dodd para conocer las mejores prácticas al trabajar con React Testing Library. Para obtener más información sobre el uso de pruebas instantáneas dentro de una aplicación React, consulta How To Write Snapshot Tests .
Deja una respuesta