Fala galera! Nos artigos anteriores criamos dois componentes e uma função, testamos cada uma das partes de forma isolada, criando testes que são conhecidos como testes unitários.
Chegou a hora de testar se tudo isso funciona junto, para isso vamos desenvolver uma aplicação simples, criando outro tipo de teste, testes de integração.
Conhecendo a aplicação
A aplicação é um conversor de moedas, o usuário deve preencher uma quantia e selecionar a moeda dela, quando clicar no botão, a quantia convertida em Real Brasileiro deve ser exibida.
Para continuar de onde paramos você pode baixar o repositório e fazer um checkout
na branch exercise-04
.
Crie o arquivo App.test.js
dentro de src
e adicione o seguinte:
import { render } from "@testing-library/react";
import App from "./App";
test("shows the amount in brazilian real after submit", () => {
render(<App />);
});
Execute o comando npm test -- --watch
, e em seguida crie o arquivo App.js
dentro de src
.
const App = () => null;
export default App;
O componente vai funcionar da seguinte forma:
- Renderiza os componentes
Form
eHighlight
; - O usuário preenche as informações no formulário;
- As informações são enviadas para uma API que retorna a cotação;
- A função
multiply
multiplica o valor pela cotação; - O valor retornado pela função
multiply
é salvo em um estado e atualizado no componente.
Faça as seguintes alterações no arquivo App.test.js
:
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import App from "./App";
test("shows the amount in brazilian real after submit", () => {
render(<App />);
userEvent.type(screen.getByLabelText(/valor/i), "3000");
userEvent.selectOptions(screen.getByLabelText(/moeda/i), "USD");
userEvent.click(screen.getByRole("button", { name: /calcular/i }));
expect(screen.getByText("14808.9")).toBeInTheDocument();
});
Dessa forma, após o render
, o formulário é preenchido com o userEvent
e após o clique do botão verificamos se o valor esperado está sendo exibido.
O teste deve retornar o seguinte erro:
TestingLibraryElementError: Unable to find a label with the text of: /valor/i
Para teste passe, faça as seguintes alterações no arquivo App.js
:
import Form from "./components/Form";
import Highlight from "./components/Highlight";
const App = () => {
const handleSubmit = () => null;
return (
<>
<Form onSubmit={handleSubmit} />
<Highlight value="14808.90" />
</>
)
};
Agora precisamos refatorar o código, para que tenha uma implementação que funcione realmente. Para isso, vamos utilizar a API de Cotações de Moedas para obter a cotação.
Refatoração
Faça as seguintes alterações no arquivo App.js
.
import { useState } from "react";
import Form from "./components/Form";
import Highlight from "./components/Highlight";
import { multiply } from './utils/math'
const App = () => {
const [value, setValue] = useState(null);
const handleSubmit = async ({ value, coin }) => {
const res = await window.fetch(
`https://economia.awesomeapi.com.br/json/last/${coin}-BRL`
);
const data = await res.json();
const result = multiply(data[`${coin}BRL`].ask, value).toFixed(2);
setValue(result);
};
return (
<>
<Form onSubmit={handleSubmit} />
<Highlight value={value} />
</>
)
};
Com essas alterações o handleSubmit
envia os dados, multiplica os valores e armazena o valor em um estado, exatamente como descrevemos antes.
Teoricamente os testes deveriam estar passando, mas não estão, repare que aparecem várias mensagens de erro, e uma delas é essa:
TypeError: window.fetch is not a function
Isso acontece porque os testes são executados em um ambiente Node.js, onde não existe uma implementação nativa para a Fetch API.
Para resolver isso, é necessário usar um polyfill, você pode instalar usando o seguinte comando:
npm install --save whatwg-fetch
Após a instalação, adicione a seguinte linha no arquivo jest.config.js
:
setupFiles: [require.resolve('whatwg-fetch')],
Isso é o suficiente para que possamos usar o fetch
no ambiente de testes. Se você olhar no terminal, os erros sobre o fetch
não aparecem mais, o único problema é o teste falhando, mas já vamos resolver isso.
Testando fluxos assíncronos
Observe onde o teste falha:
> 12 | expect(screen.getByText("14808.90")).toBeInTheDocument();
Observe também, que a div
onde o valor deveria estar, está vazia:
<body>
<div>
<form>...</form>
<div />
</div>
</body>
Nenhum valor foi exibido porque o expect
foi executado antes da requisição ser concluída. Para resolver isso, precisamos fazer duas alterações.
Primeiro, o bloco do teste deve ser assíncrono:
test("shows the amount in brazilian real after submit", async () => {
render(<App />);
E segundo, a asserção deve ser feita após a requisição ser concluída para exibir o valor esperado. Podemos usar uma query do tipo findBy
ao invés de getBy
, queries desse tipo são assíncronas.
expect(await screen.findByText("14808.90")).toBeInTheDocument();
Criando um mock para a Fetch API
Nesse momento, provavelmente o teste está falhando, e eu falo provavelmente porque é algo incerto. Estamos fazendo um requisição para uma API externa, que retorna valores que mudam constantemente.
Nesse tipo de teste, não devemos nos fazer requisições para nenhum serviço externo. Então, ao invés de chamar o fetch
original, precisamos de algo que funcione no lugar dele, um mock
.
Dessa vez vamos utilizar o método spyOn
dentro de um beforeAll
. Adicione a seguinte linha antes da função test
no arquivo App.test.js
:
beforeAll(() => jest.spyOn(window, "fetch"));
O spyOn
funciona de forma parecida com o jest.fn
, ele recebe um objeto, o nome do método que será mocado e adiciona as propriedades de uma função mock, permitindo fazer as seguintes asserções:
expect(window.fetch).toHaveBeenCalledWith(
"https://economia.awesomeapi.com.br/json/last/USD-BRL"
);
expect(window.fetch).toHaveBeenCalledTimes(1);
Só isso não é o suficiente, porque o fetch
original continua sendo chamado. Para que isso não aconteça, podemos usar o método mockImplementationOnce
para definir uma implementação própria para o fetch
. Adicione o seguinte código após a chamada do render
no teste:
window.fetch.mockImplementationOnce(() => Promise.resolve({
ok: true,
json: async () => ({
USDBRL: { ask: "4.9363" },
}),
}));
Dessa forma, ao invés de chamar o fetch
original, a função que passamos para o mockImplementationOnce
é que será chamada. O Jest conta com um método chamado mockResolvedValueOnce
que é um “syntactic sugar” para o que acabamos de fazer.
window.fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
USDBRL: { ask: "4.9363" },
}),
});
Feito isso, os testes devem voltar a passar.
Testando um erro na requisição
Para testar a situação em que a API retorna um erro, podemos criar um teste parecido com o anterior, a diferença nesse caso, é que a API retorna uma mensagem de erro e verificamos se essa mensagem está sendo exibida.
test("renders an error message from the server", async () => {
const testError = "test error";
render(<App />);
window.fetch.mockResolvedValueOnce({
ok: false,
json: async () => ({ message: testError }),
});
userEvent.type(screen.getByLabelText(/valor/i), "3000");
userEvent.selectOptions(screen.getByLabelText(/moeda/i), "USD");
userEvent.click(screen.getByRole("button", { name: /calcular/i }));
expect(await screen.findByRole("alert")).toHaveTextContent(testError);
});
Para que o teste passe, faça as seguintes alterações no arquivo App.js
const App = () => {
const [value, setValue] = useState(null);
const [error, setError] = useState(null);
const handleSubmit = async ({ value, coin }) => {
const res = await window.fetch(
`https://economia.awesomeapi.com.br/json/last/${coin}-BRL`
);
const data = await res.json();
if (!res.ok) {
setError(data.message);
return;
}
const result = multiply(data[`${coin}BRL`].ask, value).toFixed(2);
setValue(result);
};
return (
<>
<Form onSubmit={handleSubmit} />
<Highlight value={value} />
{error && <div role="alert">{error}</div>}
</>
)
};
Conclusão
Nesse artigo, criamos testes de integração para verificar se os componentes que foram desenvolvidos nos artigos anteriores funcionam juntos em uma aplicação, adicionamos suporte para a Fetch API e mais uma vez criamos mocks.
Em aplicações Front-End, testes unitários e testes de integração são bem parecidos, a diferença é que nos testes de integração podemos testar fluxos bem maiores, como páginas ou até mesmo uma aplicação completa, como nesse caso.
Essa série é algo que estou gostando bastante de fazer, espero que este conteúdo esteja ajudando, qualquer coisa não deixa de comentar.
Como de costume, aqui o repositório para com código feito nesse artigo. Abraço!