Pruebas de componentes en la práctica

Las pruebas de componentes son un buen punto de partida para 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 pensaran en los sitios web o las aplicaciones web como compuestos de componentes.

Por lo tanto, probar componentes individuales, independientemente de su complejidad, 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, haciendo clic en un botón y confirmando que aumenta un número. En realidad, muy poco código es así, y probar código que no tiene interacciones puede ser de valor limitado.

El componente que se está probando

Usamos Vitest y su entorno JSDOM para probar un componente de React. Esto nos permite ejecutar pruebas rápidamente con Node en la línea de comandos mientras emulamos un navegador.

Una lista de nombres con un botón Elegir junto a cada uno.
Un pequeño componente 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 un useEffect, y Context pasa el controlador de selección. Este es su 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 demuestran las prácticas recomendadas de React (por ejemplo, usa fetch dentro de useEffect), pero es probable que tu base de código contenga muchos casos como este. Más concretamente, estos casos pueden parecer difíciles de probar a primera vista. En una sección futura de este curso, se analizará en detalle la escritura de código que se pueda probar.

Aquí están los elementos que estamos probando en este ejemplo:

  • Comprueba que se creen algunos DOM correctos en respuesta a los datos de la red.
  • Confirma que hacer clic en un usuario active una devolución de llamada.

Cada componente es diferente. ¿Por qué es interesante probar esta opción?

  • Usa el fetch global para solicitar datos reales de la red, que podrían ser inestables o lentas cuando se somete a prueba.
  • Importa otra clase, UserRow, que quizás no queramos probar de forma implícita.
  • Usa un Context que no forma parte específicamente 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 que quede claro, este ejemplo no es muy útil. Sin embargo, es útil configurar el modelo de texto en un archivo de par llamado UserList.test.tsx (recuerda que, de forma predeterminada, los ejecutores de pruebas como Vitest ejecutan archivos que terminan en .test.js o similares, incluida .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 "Usuarios". Funciona aunque el componente tenga un efecto secundario de enviar un 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 finalice la prueba, al menos no sin esperar un tiempo de espera.

fetch() simulado

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

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

En vitest, puedes simular un elemento 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 examinan con más detalle en una sección posterior de este curso, pero puedes verlos en la 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 una simulación, 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 cambiando la consulta en queryByText a un nombre nuevo), la prueba fallará.

En este ejemplo, se usaron los ayudantes de simulación integrados de Vitest, pero otros marcos de trabajo de pruebas tienen enfoques similares a la simulación. Vitest es único en el sentido de que debes llamar a vi.unstubAllGlobals() después de todas las pruebas o establecer una opción global equivalente. Sin "deshacer" nuestro trabajo, el simulador 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 busca "Sam", y eso no se renderiza directamente dentro de UserList, por lo que debe provenir de UserRow.

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

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

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

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

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

Al igual que la simulación de fetch global, esta es una herramienta potente, pero puede ser insostenible si tu código tiene muchas dependencias. Una vez más, la mejor solución 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 UserContext, que invoca el método si se elige un usuario. Esto suele considerarse una alternativa al "desglose de propiedades", en el que la devolución de llamada se pasa directamente a UserList.

Nuestro agente de prueba no proporcionó UserContext. Si agregas una acción de clic a la prueba de React sin ella, es posible que, en el peor de los casos, la prueba falle. En el mejor de los casos, si se proporciona una instancia predeterminada en otro lugar, es posible que se produzca algún comportamiento fuera de nuestro control (similar a un UserRow desconocido mencionado anteriormente).

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

En cambio, cuando renderices el componente, puedes proporcionar tu propio Context. En este ejemplo, se usa una instancia de vi.fn(), una función de simulación de Vitest, que se puede usar para verificar que se realizó una llamada y qué argumentos se usaron.

En nuestro caso, esto interactúa con el fetch simulado en el ejemplo anterior, y la prueba puede confirmar que el ID que se pasó fue 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.

Resumen

En este ejemplo, se mostró cómo compilar una prueba de componentes para probar y proteger un componente de React difícil de probar. Esta prueba se enfocó en garantizar que el componente interactúe correctamente con sus dependencias: el fetch global, un subcomponente importado y un Context.

Verifica tu comprensión

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

Inserción de dependencias con Context
Cómo simular dependencias complejas con otras simples para pruebas
Globalización de stub
Cómo verificar que un número haya aumentado