Pruebas de componentes en la práctica

La prueba de componentes es un buen lugar para comenzar a demostrar código de prueba práctico. Las pruebas de componentes son más sustanciales que las pruebas de unidades simples, menos complejas que las pruebas de extremo a extremo y demuestran la interacción con el DOM. Desde un punto de vista más filosófico, el uso de React facilitó que los desarrolladores web piensen en los sitios web o las apps web como componentes de componentes.

Por lo tanto, probar los componentes individuales, sin importar qué tan complejos sean, es una buena manera de comenzar a pensar en probar una aplicación nueva o existente.

En esta página, se explica cómo probar un componente pequeño con dependencias externas complejas. Es fácil probar un componente que no interactúa con ningún otro código, por ejemplo, cuando haces clic en un botón y confirmas que un número aumenta. En realidad, muy poco código es así, y probar el código que no tiene interacciones puede tener un valor limitado.

Este instructivo no es un instructivo completo. En la sección posterior, Pruebas automatizadas en la práctica, se explicará cómo probar un sitio real con un código de muestra que puedes usar como instructivo. Sin embargo, en esta página, se tratarán varios ejemplos prácticos de prueba de componentes).

El componente a prueba

Usaremos Vitest y su entorno JSDOM para probar un componente de React. Esto nos permite ejecutar pruebas con rapidez mediante Node en la línea de comandos mientras se emula un navegador.

Una lista de nombres con un botón de selección junto a cada uno.
Un componente pequeño de React que muestra una lista de usuarios de la red

Este componente de React llamado UserList recupera una lista de usuarios de la red y te permite seleccionar uno de ellos. La lista de usuarios se obtiene con fetch dentro de una useEffect, y Context pasa el controlador de selección. Este es el código:

import React, { useEffect, useState, useContext } from 'react';
import { UserContext } from './UserContext.tsx';
import { UserRow } from './UserRow.tsx';

export function UserList({ count = 4 }: { count?: number }) {
  const [users, setUsers] = useState<any[]>([]);
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users?_limit=' + count)
      .then((response) => response.json())
      .then((json) => setUsers(json));
  }, [count]);

  const c = useContext(UserContext);
  return (
    <div>
      <h2>Users</h2>
      <ul>
        {users.map((u) => (
          <li key={u.id}>
            <button onClick={() => c.userChosen(u.id)}>Choose</button>{' '}
            <UserRow u={u} />
          </li>
        ))}
      </ul>
    </div>
  );
}

En este ejemplo, no se muestran las prácticas recomendadas de React (por ejemplo, se usa fetch dentro de useEffect), pero es probable que tu base de código contenga muchos casos similares. Además, estos casos pueden parecer difíciles de probar a primera vista. En una sección posterior de este curso, se analizará en detalle la escritura de código que se pueda probar.

Estos son los elementos que estamos probando en este ejemplo:

  • Verifica que se creen algunos DOM correctos en respuesta a los datos de la red.
  • Confirma que, cuando se haga clic en un usuario, se activa una devolución de llamada.

Cada componente es diferente. ¿Por qué es interesante probar este?

  • Usa el fetch global para solicitar datos reales de la red, que podrían ser inestables o lentas en la etapa de prueba.
  • Importa otra clase, UserRow, que quizás no queramos probar de forma implícita.
  • Usa un Context que no es específicamente parte del código que se está probando y, por lo general, lo proporciona un componente superior.

Escribe una prueba rápida para comenzar

Podemos probar rápidamente algo muy básico sobre este componente. Para ser claros, este ejemplo no es muy útil. Sin embargo, es útil configurar el código estándar en un archivo de pares llamado UserList.test.tsx (recuerda que, de forma predeterminada, los ejecutores de pruebas como Vitest ejecutarán archivos que terminen en .test.js o uno similar, incluido .tsx):

import { vi, test, assert, afterAll } from 'vitest';
import { render } from '@testing-library/react';
import { UserList } from './UserList.tsx';
import React, { ContextType } from 'react';

test('render', async () => {
  const c = render(<UserList />);

  const headingNode = await c.findAllByText(/Users);
  assert.isNotNull(headingNode);
});

Esta prueba afirma que, cuando se renderiza el componente, contiene el texto "Users". Funciona aunque el componente tenga el efecto secundario de enviar fetch a la red. El fetch aún está en curso al final de la prueba, sin un extremo establecido. No podemos confirmar que se muestre información del usuario cuando finaliza la prueba, al menos no sin esperar a que se agote el tiempo de espera.

fetch() de prueba

La simulación es el acto de reemplazar una función o clase real por algo que tú controlas para una prueba. Esta es una práctica común en casi todos los tipos de pruebas, excepto las de unidades más simples. Esto se abordará con más detalle en Aserciones y otras primitivas.

Puedes simular fetch() para tu prueba, de modo que se complete rápidamente y muestre los datos que esperas, y no datos “reales” o desconocidos. fetch es global, lo que significa que no tenemos que import ni require en nuestro código.

En vitest, puedes simular un global llamando a vi.stubGlobal con un objeto especial que muestra vi.fn(); esto compila una simulación que podemos modificar más adelante. Estos métodos se analizarán con más detalle en una sección posterior de este curso, pero puedes verlos en práctica en el siguiente código:

test('render', async () => {
  const fetchMock = vi.fn();
  fetchMock.mockReturnValue(
    Promise.resolve({
      json: () => Promise.resolve([{ name: 'Sam', id: 'sam' }]),
    }),
  );
  vi.stubGlobal('fetch', fetchMock);

  const c = render(<UserList />);

  const headingNode = await c.queryByText(/Users);
  assert.isNotNull(headingNode);

  await waitFor(async () => {
    const samNode = await c.queryByText(/Sam);
    assert.isNotNull(samNode);
  });
});

afterAll(() => {
  vi.unstubAllGlobals();
});

Este código agrega un modelo de prueba, describe una versión "falsa" de la recuperación de red Response y, luego, espera a que aparezca. Si el texto no aparece (puedes verificar esto si cambias la consulta de queryByText a un nombre nuevo), la prueba fallará.

En este ejemplo, se usaron los asistentes de simulación integrados de Vitest, pero otros frameworks de prueba tienen enfoques similares para la simulación. Vitest es único, ya que debes llamar a vi.unstubAllGlobals() después de todas las pruebas o establecer una opción global equivalente. Sin "deshacer" nuestro trabajo, la simulación de fetch puede afectar otras pruebas, y cada solicitud se responderá con nuestra pila extraña de JSON.

Importaciones de prueba

Es posible que hayas notado que nuestro componente UserList importa un componente llamado UserRow. Si bien no incluimos su código, puedes ver que renderiza el nombre del usuario: la prueba anterior verifica "Sam" y no se renderiza directamente dentro de UserList, por lo que debe provenir de UserRow.

Diagrama de flujo de cómo se mueven los nombres de los usuarios a través de nuestro componente.
UserListTest no tiene visibilidad de UserRow.

Sin embargo, UserRow podría ser un componente complejo: podría recuperar más datos del usuario o tener efectos secundarios que no son relevantes para nuestra prueba. Quitar esa variabilidad hará que las pruebas sean más útiles, en especial a medida que los componentes que deseas probar se vuelven más complejos y se entrelazan más con sus dependencias.

Afortunadamente, puedes usar Vitest para simular ciertas importaciones, incluso si tu prueba no las usa directamente, de modo que cualquier código que las use se proporcione con una versión simple o conocida:

vi.mock('./UserRow.tsx', () => {
  return {
    UserRow(arg) {
      return <>{arg.u.name}</>;
    },
  }
});

test('render', async () => {
  // ...
});

Al igual que simular el fetch global, esta es una herramienta potente, pero puede volverse insostenible si tu código tiene muchas dependencias. Una vez más, la mejor solución para eso es escribir código que se pueda probar.

Haz clic y proporciona contexto.

React y otras bibliotecas como Lit tienen un concepto llamado Context. El código de muestra incluye un UserContext que invoca el método si se selecciona a un usuario. Esto se suele ver como una alternativa al "desglose de prop", en el que la devolución de llamada se pasa directamente a UserList.

El agente de prueba que escribimos no proporcionó UserContext. Si agregas una acción de clic a la prueba de React sin ella, la prueba fallará en el peor o, en el mejor de los casos, si se proporcionó una instancia predeterminada en otro lugar, provocará un comportamiento fuera de nuestro control (similar a un UserRow desconocido que aparece más arriba):

  const c = render(<UserList />);
  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();

En su lugar, cuando renderizas el componente, puedes proporcionar tu propio Context. En este ejemplo, se usa una instancia de vi.fn(), una función de Vitest Mock, que se puede usar después del hecho de verificar que se haya realizado una llamada y con qué argumentos se realizó. En nuestro caso, esto interactúa con la fetch simulada del ejemplo anterior, y la prueba puede confirmar que el ID que se pasó era "sam":

  const userChosenFn = vi.fn();
  const ucForTest: ContextType<typeof UserContext> = { userChosen: userChosenFn as any };
  const c = render(
    <UserContext.Provider value={ucForTest}>
      <UserList />
    </UserContext.Provider>,
  );

  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();
  assert.deepStrictEqual(userChosenFn.mock.calls, [['sam']]);

Este es un patrón simple, pero potente que puede permitirte quitar dependencias irrelevantes del componente principal que intentas probar.

En resumen

Este fue un ejemplo rápido y simplificado en el que se demuestra cómo compilar una prueba de componente para probar y proteger un componente de React difícil de probar, que se enfoca en garantizar que el componente interactúe correctamente con sus dependencias (el fetch global, un subcomponente importado y un Context).

Verifica tus conocimientos

¿Qué enfoques se utilizaron para probar el componente de React?

Simulación de dependencias complejas con dependencias simples para pruebas
Inserción de dependencias con contexto
Globales de Stubbing
Comprobar que un número aumentó