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