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