Aumente la capacidad de mantenimiento del código con las pruebas de integración de React

Publicado: 2022-03-11

Las pruebas de integración son un punto óptimo entre el costo y el valor de las pruebas. Escribir pruebas de integración para una aplicación React con la ayuda de react-testing-library en lugar de o además de las pruebas unitarias de componentes puede aumentar la capacidad de mantenimiento del código sin afectar la velocidad de desarrollo.

En caso de que desee obtener una ventaja antes de continuar, puede ver un ejemplo de cómo usar react-testing-library para las pruebas de integración de la aplicación React aquí.

¿Por qué invertir en pruebas de integración?

"Las pruebas de integración logran un gran equilibrio en las compensaciones entre la confianza y la velocidad/gasto. Es por eso que es recomendable dedicar la mayor parte (no todo, eso sí) de su esfuerzo allí".
– Kent C. Dodds en Pruebas de escritura. No muchos. Sobre todo integración.

Es una práctica común escribir pruebas unitarias para los componentes de React, a menudo utilizando una biblioteca popular para probar las "enzimas" de React; específicamente, su método "superficial". Este enfoque nos permite probar los componentes de forma aislada del resto de la aplicación. Sin embargo, dado que escribir aplicaciones React se trata de componer componentes, las pruebas unitarias por sí solas no garantizan que la aplicación esté libre de errores.

Por ejemplo, cambiar los accesorios aceptados de un componente y actualizar sus pruebas unitarias asociadas puede dar como resultado que todas las pruebas pasen, mientras que la aplicación aún puede fallar si otro componente no se actualizó en consecuencia.

Las pruebas de integración pueden ayudar a preservar la tranquilidad al realizar cambios en una aplicación React, ya que garantizan que la composición de los componentes dé como resultado la experiencia de usuario deseada.

Requisitos para las pruebas de integración de la aplicación React

Estas son algunas de las cosas que los desarrolladores de React quieren hacer al escribir pruebas de integración:

  • Pruebe los casos de uso de la aplicación desde la perspectiva del usuario. Los usuarios acceden a la información de una página web e interactúan con los controles disponibles.
  • Simule las llamadas a la API para no depender de la disponibilidad y el estado de la API para pasar/reprobar las pruebas.
  • Simular las API del navegador (por ejemplo, el almacenamiento local) ya que simplemente no existen en el entorno de prueba.
  • Afirmar en el estado React DOM (DOM del navegador o un entorno móvil nativo).

Ahora, algunas cosas que debemos tratar de evitar al escribir las pruebas de integración de la aplicación React:

  • Detalles de implementación de prueba. Los cambios de implementación solo deberían romper una prueba si de hecho introdujeron un error.
  • Burlarse demasiado. Queremos probar cómo funcionan juntas todas las partes de la aplicación.
  • renderizado superficial. Queremos probar la composición de todos los componentes de la aplicación hasta el componente más pequeño.

¿Por qué elegir React-testing-library?

Los requisitos antes mencionados hacen que la biblioteca de pruebas de reacción sea una excelente opción, ya que su principio rector principal es permitir que los componentes de React se prueben de una manera que se asemeje a cómo los usa un ser humano real.

La biblioteca, junto con sus bibliotecas complementarias opcionales, nos permite escribir pruebas que interactúan con DOM y confirman su estado.

Ejemplo de configuración de la aplicación

La aplicación para la que vamos a escribir pruebas de integración de muestra implementa un escenario simple:

  • El usuario ingresa un nombre de usuario de GitHub.
  • La aplicación muestra una lista de repositorios públicos asociados con el nombre de usuario ingresado.

La forma en que se implementa la funcionalidad anterior debería ser irrelevante desde la perspectiva de las pruebas de integración. Sin embargo, para mantenerse cerca de las aplicaciones del mundo real, la aplicación sigue los patrones comunes de React, por lo tanto, la aplicación:

  • Es una aplicación de una sola página (SPA).
  • Realiza solicitudes de API.
  • Tiene gestión de estado global.
  • Apoya la internacionalización.
  • Utiliza una biblioteca de componentes React.

El código fuente para la implementación de la aplicación se puede encontrar aquí.

Escribir pruebas de integración

Instalación de dependencias

Con hilo:

 yarn add --dev jest @testing-library/react @testing-library/user-event jest-dom nock

O con npm:

 npm i -D jest @testing-library/react @testing-library/user-event jest-dom nock

Creación de un archivo de conjunto de pruebas de integración

Crearemos un archivo llamado viewGitHubRepositoriesByUsername.spec.js en la carpeta ./test de nuestra aplicación. Jest lo recogerá automáticamente.

Importación de dependencias en el archivo de prueba

 import React from 'react'; // so that we can use JSX syntax import { render, cleanup, waitForElement } from '@testing-library/react'; // testing helpers import userEvent from '@testing-library/user-event' // testing helpers for imitating user events import 'jest-dom/extend-expect'; // to extend Jest's expect with DOM assertions import nock from 'nock'; // to mock github API import { FAKE_USERNAME_WITH_REPOS, FAKE_USERNAME_WITHOUT_REPOS, FAKE_BAD_USERNAME, REPOS_LIST } from './fixtures/github'; // test data to use in a mock API import './helpers/initTestLocalization'; // to configure i18n for tests import App from '../App'; // the app that we are going to test

Configuración de la suite de pruebas

 describe('view GitHub repositories by username', () => { beforeAll(() => { nock('https://api.github.com') .persist() .get(`/users/${FAKE_USERNAME_WITH_REPOS}/repos`) .query(true) .reply(200, REPOS_LIST); }); afterEach(cleanup); describe('when GitHub user has public repositories', () => { it('user can view the list of public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { // arrange // act // assert }); }); });

Notas:

  • Antes de todas las pruebas, simule la API de GitHub para devolver una lista de repositorios cuando se llame con un nombre de usuario específico.
  • Después de cada prueba, limpie el React DOM de prueba para que cada prueba comience desde un lugar limpio.
  • Los bloques describe especifican el caso de uso de la prueba de integración y las variaciones de flujo.
  • Las variaciones de flujo que estamos probando son:
    • El usuario ingresa un nombre de usuario válido que tiene repositorios públicos de GitHub asociados.
    • El usuario ingresa un nombre de usuario válido que no tiene repositorios públicos de GitHub asociados.
    • El usuario ingresa un nombre de usuario que no existe en GitHub.
  • bloquea it uso de devolución de llamada asíncrona ya que el caso de uso que están probando tiene un paso asíncrono.

Escribir la primera prueba de flujo

Primero, la aplicación debe renderizarse.

 const { getByText, getByPlaceholderText, queryByText } = render(<App />);

El método de render importado del módulo @testing-library/react renderiza la aplicación en el DOM de React de prueba y devuelve las consultas DOM vinculadas al contenedor de la aplicación renderizada. Estas consultas se utilizan para ubicar elementos DOM para interactuar y afirmar.

Ahora, como primer paso del flujo bajo prueba, al usuario se le presenta un campo de nombre de usuario y escribe una cadena de nombre de usuario en él.

 userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS);

El asistente userEvent del módulo @testing-library/user-event importado tiene un método de type que imita el comportamiento del usuario cuando escribe texto en un campo de texto. Acepta dos parámetros: el elemento DOM que acepta la entrada y la cadena que escribe el usuario.

Los usuarios suelen encontrar elementos DOM por el texto asociado a ellos. En el caso de la entrada, es texto de etiqueta o texto de marcador de posición. El método de consulta getByPlaceholderText devuelto anteriormente desde render nos permite encontrar el elemento DOM por texto de marcador de posición.

Tenga en cuenta que, dado que es probable que el texto en sí mismo cambie, es mejor no confiar en los valores de localización reales y, en su lugar, configurar el módulo de localización para devolver una clave de elemento de localización como su valor.

Por ejemplo, cuando la localización "en-US" normalmente devolvería Enter GitHub username como valor para la clave userSelection.usernamePlaceholder , en las pruebas, queremos que devuelva userSelection.usernamePlaceholder .

Cuando el usuario escribe texto en un campo, debería ver el valor del campo de texto actualizado.

 expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS);

A continuación en el flujo, el usuario hace clic en el botón Enviar y espera ver la lista de repositorios.

 userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header');

El método userEvent.click imita al usuario haciendo clic en un elemento DOM, mientras que la consulta getByText encuentra un elemento DOM por el texto que contiene. El modificador closest asegura que seleccionemos el elemento del tipo correcto.

Nota: En las pruebas de integración, los pasos a menudo sirven tanto para act como para assert . Por ejemplo, afirmamos que el usuario puede hacer clic en un botón haciendo clic en él.

En el paso anterior, afirmamos que el usuario ve la sección de la lista de repositorios de la aplicación. Ahora, debemos afirmar que, dado que obtener la lista de repositorios de GitHub puede llevar algún tiempo, el usuario ve una indicación de que la obtención está en curso. También queremos asegurarnos de que la aplicación no le diga al usuario que no hay repositorios asociados con el nombre de usuario ingresado mientras se sigue recuperando la lista de repositorios.

 getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull();

Tenga en cuenta que el prefijo de consulta getBy se usa para afirmar que se puede encontrar el elemento DOM y el prefijo de consulta queryBy es útil para la afirmación opuesta. Además, queryBy no devuelve un error si no se encuentra ningún elemento.

A continuación, queremos asegurarnos de que, eventualmente, la aplicación termine de buscar repositorios y se los muestre al usuario.

 await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, []));

El método asincrónico waitForElement se usa para esperar una actualización de DOM que hará que la aserción proporcionada como parámetro de método sea verdadera. En este caso, afirmamos que la aplicación muestra el nombre y la descripción de cada repositorio devuelto por la API de GitHub simulada.

Finalmente, la aplicación ya no debería mostrar un indicador de que se están recuperando los repositorios y no debería mostrar un mensaje de error.

 expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull();

Nuestra prueba de integración de React resultante se ve así:

 it('user can view the list of public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, [])); expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull(); });

Pruebas de flujo alternativo

Cuando el usuario ingresa un nombre de usuario de GitHub sin repositorios públicos asociados, la aplicación muestra un mensaje apropiado.

 describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITHOUT_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITHOUT_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.empty')); expect(queryByText('repositories.error')).toBeNull(); }); });

Cuando el usuario ingresa un nombre de usuario de GitHub que no existe, la aplicación muestra un mensaje de error.

 describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_BAD_USERNAME); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_BAD_USERNAME); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.error')); expect(queryByText('repositories.empty')).toBeNull(); }); });

Por qué las pruebas de integración de React son geniales

Las pruebas de integración realmente ofrecen un punto óptimo para las aplicaciones React. Estas pruebas ayudan a detectar errores y utilizan el enfoque TDD mientras que, al mismo tiempo, no requieren mantenimiento cuando cambia la implementación.

React-testing-library, que se muestra en este artículo, es una excelente herramienta para escribir pruebas de integración de React, ya que le permite interactuar con la aplicación como lo hace el usuario y validar el estado y el comportamiento de la aplicación desde la perspectiva del usuario.

Con suerte, los ejemplos proporcionados aquí lo ayudarán a comenzar a escribir pruebas de integración en proyectos React nuevos y existentes. El código de muestra completo que incluye la implementación de la aplicación se puede encontrar en mi GitHub.