Testes de componentes na prática

O teste de componentes é um bom ponto de partida para a demonstração do código de teste prático. Os testes de componentes são mais substanciais do que os simples testes de unidade, menos complexos do que os testes de ponta a ponta e demonstram a interação com o DOM. Mais filosoficamente, 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, independente da complexidade deles, é uma boa maneira de começar a testar um aplicativo novo ou atual.

Nesta página, mostramos como testar 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 se um número aumenta. Na realidade, muito pouco código é assim, e testar códigos que não têm interações podem ter valor limitado.

Este não é um tutorial completo. Na próxima seção, Teste automatizado na prática, você vai conferir como fazer testes em um site real com um exemplo de código que pode ser usado como tutorial. No entanto, esta página ainda aborda vários exemplos de testes práticos de componentes.

O componente em teste

Usaremos o Vitest e o ambiente JSDOM para testar um componente do React. Isso permite executar testes rapidamente usando o Node na linha de comando durante a emulação de 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 na rede e permite que você selecione um deles. A lista de usuários é recebida usando fetch dentro de um 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>
  );
}

Este exemplo não demonstra as práticas recomendadas do React (por exemplo, ele usa fetch dentro de useEffect), mas sua base de código provavelmente contém muitos casos semelhantes. Mais especificamente, esses casos podem parecer teimosos para serem testados à primeira vista. Uma seção futura deste curso discutirá a criação de códigos testáveis em detalhes.

Estas são as coisas que estamos testando neste exemplo:

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

Cada componente é diferente. O que torna este teste interessante?

  • Ela usa o fetch global para solicitar dados reais da rede, que podem estar instáveis ou lentos durante o teste.
  • Ele importa outra classe, UserRow, que talvez não queiramos testar implicitamente.
  • Ela 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. Esclarecendo, esse exemplo não é muito útil. No entanto, é útil configurar o código boilerplate em um arquivo de peering chamado UserList.test.tsx. Lembre-se de que executores de teste como o Vitest vão, por padrão, executar 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 declara que, quando o componente é renderizado, ele contém o texto "Users". Isso funciona mesmo que o componente tenha um efeito colateral de enviar um fetch para a rede. O fetch ainda está em andamento ao final do teste, sem um endpoint definido. Não é possível confirmar se as informações do usuário estão sendo exibidas quando o teste termina, pelo menos não sem aguardar um tempo limite.

Simulação de fetch()

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 teste, exceto nos mais simples. Isso será abordado em mais detalhes em Declarações e outros primitivos.

É possível simular fetch() para seu 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 precisamos incluir import ou require no código.

No Vitest, você pode simular um objeto global chamando vi.stubGlobal com um objeto especial retornado por vi.fn(). Isso cria uma simulação que pode ser modificada mais tarde. Esses métodos serão examinados em mais detalhes em uma seção posterior deste curso, mas você pode vê-los na prática no seguinte 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();
});

Esse código adiciona uma simulação, descreve uma versão "falso" da busca de rede Response e espera que ela apareça. Se o texto não aparecer, verifique isso mudando a consulta em queryByText para um novo nome, o teste 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 é exclusivo, porque você precisa chamar vi.unstubAllGlobals() depois de todos os testes ou definir uma opção global equivalente. Sem "desfazer" nosso trabalho, a simulação de fetch pode afetar outros testes, e todas as solicitações serão respondidas com nossa pilha estranha de JSON.

Importações simuladas

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

Um fluxograma de como
  os nomes dos usuários se movem pelo nosso 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 nosso teste. Remover essa variabilidade tornará seus testes mais úteis, especialmente porque os componentes que você quer testar ficam mais complexos e mais interligados com as dependências.

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

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

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

Assim como a simulação do fetch global, essa é uma ferramenta poderosa, mas poderá se tornar insustentável se o código tiver muitas dependências. Novamente, a melhor correção para isso é escrever um código testável.

Clique e forneça contexto

O React e outras bibliotecas, como a Lit (link em inglês), têm um conceito chamado Context. O exemplo de código inclui um UserContext que invoca um método se um usuário for escolhido. Geralmente, isso é visto como uma alternativa ao "detalhamento de prospecção", em que o callback é transmitido diretamente para UserList.

O arcabouço de testes que criamos não forneceu UserContext. Adicionar uma ação de clique ao teste do React sem essa ação causa, na pior das hipóteses, uma falha no teste ou, melhor, se uma instância padrão foi fornecida em outro lugar, causa um 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 simulada do Vitest, que pode ser usada após a verificação para verificar se uma chamada foi feita e com quais argumentos ela foi feita. No nosso caso, isso 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 eficiente, que permite remover dependências irrelevantes do componente principal que você está tentando testar.

Em resumo

Este foi um exemplo rápido e simplificado que demonstra como criar um teste de componente para testar e proteger um componente do React difícil de testar, com foco 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?

Simulação de dependências complexas com dependências simples para teste
Injeção de dependência usando contexto
Agrupamentos globais
Verificar se um número foi incrementado