Testes de componentes na prática

O teste de componentes é um bom lugar para começar a demonstrar o código de teste prático. Os testes de componentes são mais substanciais do que testes de unidade simples, menos complexos do que testes de ponta a ponta e demonstram interação com o DOM. Em termos filosóficos, o uso do React facilitou para os desenvolvedores da Web pensar em sites ou apps da Web como compostos de componentes.

Portanto, testar componentes individuais, independentemente da complexidade, é uma boa maneira de começar a pensar em testar um aplicativo novo ou existente.

Esta página orienta o teste de um pequeno componente com dependências externas complexas. É fácil testar um componente que não interage com nenhum outro código, como clicar em um botão e confirmar que um número aumenta. Na realidade, muito pouco código é assim, e testar código que não tem interações pode ser de valor limitado.

O componente em teste

Usamos o Vitest e o ambiente JSDOM dele para testar um componente do React. Isso permite executar testes rapidamente usando o Node na linha de comando enquanto emula um navegador.

Uma lista de nomes com um botão "Escolher" ao lado de cada um.
Um pequeno componente do React que mostra uma lista de usuários da rede.

Esse componente do React chamado UserList busca uma lista de usuários da rede e permite que você selecione um deles. A lista de usuários é obtida usando fetch dentro de uma useEffect, e o gerenciador de seleção é transmitido por Context. Este é o 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>
  );
}

Esse exemplo não demonstra as práticas recomendadas do React (por exemplo, ele usa fetch dentro de useEffect), mas é provável que sua base de código contenha muitos casos semelhantes. De fato, esses casos podem parecer difíceis de testar à primeira vista. Uma seção futura deste curso vai discutir a criação de código testável em detalhes.

Confira o que estamos testando neste exemplo:

  • Verifique se algum DOM correto é criado em resposta aos dados da rede.
  • Confirme se o clique em um usuário aciona um callback.

Cada componente é diferente. O que torna o teste desse interessante?

  • Ela usa o fetch global para solicitar dados reais da rede, que podem ser instáveis ou lentos em teste.
  • Ele importa outra classe, UserRow, que talvez não queiramos testar implicitamente.
  • Ele usa um Context que não faz parte especificamente do código em teste e normalmente é fornecido por um componente pai.

Crie um teste rápido para começar

Podemos testar rapidamente algo muito básico sobre esse componente. Para ser claro, este exemplo não é muito útil. No entanto, é útil configurar o modelo em um arquivo semelhante chamado UserList.test.tsx. Os executores de teste, como o Vitest, executam por padrão arquivos que terminam com .test.js ou semelhantes, incluindo .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);
});

Esse teste afirma que, quando o componente é renderizado, ele contém o texto "Users". Ele funciona mesmo que o componente tenha um efeito colateral de enviar um fetch para a rede. O fetch ainda está em andamento no final do teste, sem um endpoint definido. Não podemos confirmar se as informações do usuário estão sendo mostradas quando o teste termina, pelo menos não sem esperar um tempo limite.

Modelo fetch()

A simulação é o ato de substituir uma função ou classe real por algo sob seu controle para um teste. Essa é uma prática comum em quase todos os tipos de testes, exceto nos testes unitários mais simples. Isso é abordado mais em Declarações e outras primitivas.

É possível simular fetch() para o teste para que ele seja concluído rapidamente e retorne os dados esperados, e não dados "reais" ou desconhecidos. fetch é global, o que significa que não é necessário import ou require incluí-lo no código.

No vitest, é possível simular um global chamando vi.stubGlobal com um objeto especial retornado por vi.fn(). Isso cria um mock que pode ser modificado mais tarde. Esses métodos serão examinados em mais detalhes em uma seção posterior deste curso, mas é possível vê-los na prática no código a seguir:

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();
});

Esse código adiciona um mock, descreve uma versão falsa do fetch Response da rede e aguarda a exibição dele. Se o texto não aparecer, mude a consulta em queryByText para um novo nome. Caso contrário, o teste vai falhar.

Este exemplo usou os auxiliares de simulação integrados do Vitest, mas outros frameworks de teste têm abordagens semelhantes à simulação. O Vitest é único porque você precisa chamar vi.unstubAllGlobals() após todos os testes ou definir uma opção global equivalente. Sem "desfazer" nosso trabalho, o mock fetch pode afetar outros testes, e cada solicitação será respondida com nossa pilha estranha de JSON.

Importações simuladas

Você pode ter notado que o próprio componente UserList importa um componente chamado UserRow. Embora não tenhamos incluído o código, é possível ver que ele renderiza o nome do usuário: o teste anterior verifica "Sam", que não é renderizado diretamente no UserList. Portanto, ele precisa vir de UserRow.

Um fluxograma de como
  os nomes dos usuários se movem pelo componente.
UserListTest não tem visibilidade de UserRow.

No entanto, o UserRow pode ser um componente complexo. Ele pode buscar mais dados do usuário ou ter efeitos colaterais que não são relevantes para o teste. A remoção dessa variabilidade torna seus testes mais úteis, especialmente porque os componentes que você quer testar ficam mais complexos e mais interligados com as dependências deles.

Felizmente, é possível usar o Vitest para simular determinadas importações, mesmo que o teste não as use diretamente, para que qualquer código que as use seja fornecido com uma versão simples ou conhecida:

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

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

Assim como o mock da fetch global, essa é uma ferramenta poderosa, mas ela pode se tornar inviável se o código tiver muitas dependências. Novamente, a melhor correção é escrever um código testável.

Clique e forneça contexto

O React e outras bibliotecas como o Lit têm um conceito chamado Context. O exemplo de código inclui UserContext, que invoca o método se um usuário for escolhido. Isso é muitas vezes visto como uma alternativa ao "prop drilling", em que o callback é transmitido diretamente para UserList.

Nosso arcabouço de teste não forneceu UserContext. Adicionar uma ação de clique ao teste do React sem ela pode, na pior das hipóteses, fazer com que o teste falhe. Na melhor opção, se uma instância padrão tiver sido fornecida em outro lugar, ela poderá causar algum comportamento fora do nosso controle, semelhante a um UserRow desconhecido acima.

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

Em vez disso, ao renderizar o componente, você pode fornecer seu próprio Context. Este exemplo usa uma instância de vi.fn(), uma função de simulação do Vitest, que pode ser usada para verificar se uma chamada foi feita e quais argumentos ela usou.

No nosso caso, ele interage com o fetch simulado no exemplo anterior, e o teste pode confirmar que o ID transmitido 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']]);

Esse é um padrão simples, mas poderoso, que permite remover dependências irrelevantes do componente principal que você está tentando testar.

Resumo

Esse exemplo demonstrou como criar um teste de componente para testar e proteger um componente React difícil de testar. Esse teste se concentrou em garantir que o componente interaja corretamente com as dependências: o fetch global, um subcomponente importado e um Context.

Teste seu conhecimento

Quais abordagens foram usadas para testar o componente do React?

Como stub de dados globais
Como simular dependências complexas com dependências simples para testes
Injeção de dependência usando o Contexto
Verificar se um número foi incrementado