Como criar testes de integração em aplicações React

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:

  1. Renderiza os componentes Form e Highlight;
  2. O usuário preenche as informações no formulário;
  3. As informações são enviadas para uma API que retorna a cotação;
  4. A função multiply multiplica o valor pela cotação;
  5. 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> : null}
    </>
  )
};

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!

16