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.
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
fetchglobal 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
Contextque 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.
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?