DEV Community

Felipe César
Felipe César

Posted on • Updated on • Originally published at felipecesar.dev

Como desenvolver um formulário em React com TDD

Continuando os artigos sobre testes, chegou a hora de adicionar um pouco mais de ação, vamos criar um componente de formulário, onde o usuário vai preencher os campos e clicar em um botão para submeter os dados.

Escrevendo um teste que falha

Você pode baixar o repositório e fazer o checkout na branch exercise-02 para continuar de onde paramos.

Crie o arquivo Form.test.js dentro de src/components e adicione o seguinte conteúdo:

import { render, screen } from "@testing-library/react";
import Form from "./Form";

test("submitting the form calls onSubmit with value and coin", () => {
  render(<Form />);

  screen.getByLabelText(/valor/i);
  screen.getByLabelText(/moeda/i);
  screen.getByRole("button", { name: /calcular/i });
});
Enter fullscreen mode Exit fullscreen mode

Já adicionamos queries para obter os campos do formulário, logo após a chamada da função render. Existem diferentes tipos de queries, você pode ver com mais detalhes acessando a documentação.

Também é importante saber que existe uma ordem de prioridade recomendada para usar as queries. Nesse caso decidi usar getByLabelText e getByRole.

Após criar o teste, crie o arquivo Form.js, com o seguinte conteúdo:

const Form = () => <form />

export default Form
Enter fullscreen mode Exit fullscreen mode

Execute o comando npm test -- --watch, dessa forma os testes serão executados sempre que um arquivo for modificado. Agora observe o erro:

TestingLibraryElementError: Unable to find a label with the text of: /valor/i

Ignored nodes: comments, <script />, <style />
<body>
  <div>
    <form />
  </div>
</body>

   5 |   render(<Form />);
   6 |
>  7 |   screen.getByLabelText(/valor/i);
Enter fullscreen mode Exit fullscreen mode

Sempre que utilizamos queries do tipo getBy e o elemento não é encontrado, o teste deve falhar, e além de exibir uma mensagem de erro, também é exibido o html do componente, além da linha onde o teste falhou.

Fazendo o teste passar

Para que o teste passe, precisamos fazer com que esse erro não ocorra mais, e para isso, tudo que precisamos é adicionar o campo valor no formulário.

const Form = () => (
  <form>
    <label htmlFor="value">Valor</label>
    <input type="number" id="value" />
  </form>
)
Enter fullscreen mode Exit fullscreen mode

Note que o teste continua falhando, mas o erro é outro:

>  8 |   screen.getByLabelText(/moeda/i);
Enter fullscreen mode Exit fullscreen mode

Adicione o próximo campo do formulário:

const Form = () => (
  <form>
    <label htmlFor="value">Valor</label>
    <input type="number" id="value" />

    <label htmlFor="coin">Moeda</label>
    <select id="coin">
      <option value="USD">Dolar Americano</option>
      <option value="EUR">Euro</option>
      <option value="GBP">Libra Esterlina</option>
    </select>
  </form>
)
Enter fullscreen mode Exit fullscreen mode

É importante sempre observar bem os erros retornados pelos testes.

>  9 |   screen.getByRole("button", { name: /calcular/i });
Enter fullscreen mode Exit fullscreen mode

Agora é só adicionar o botão de submit:

const Form = () => (
  <form>
    <label htmlFor="value">Valor</label>
    <input type="number" id="value" />

    <label htmlFor="coin">Moeda</label>
    <select id="coin">
      <option value="USD">Dolar Americano</option>
      <option value="EUR">Euro</option>
      <option value="GBP">Libra Esterlina</option>
    </select>

    <button type="submit">Calcular</button>
  </form>
)
Enter fullscreen mode Exit fullscreen mode

Com isso o teste já está passando, mas o formulário ainda não envia nenhuma informação.

Adicionando eventos

O componente Form deve receber uma prop chamada onSubmit, ela será uma função que deve ser chamada com os valores preenchidos no formulário quando ocorrer o evento submit.

Faça as seguintes alterações no teste:

test("submitting the form calls onSubmit with value and coin", () => {
  const handleSubmit = jest.fn();
  render(<Form onSubmit={handleSubmit} />);
Enter fullscreen mode Exit fullscreen mode

O jest.fn() retorna uma função mock, esse tipo de função também é conhecido como "spy", e como o próprio nome já diz, eles "espionam" informações secretas sobre funções. Elas possibilitam saber, quantas vezes uma função foi chamada, quais parâmetros ela recebeu, etc.

Com a função handleSubmit criada, precisamos preencher o formulário com alguns valores e clicar no botão para submeter o formulário. Para executar o evento de clique precisamos importar o fireEvent da Testing Library. Faça as seguintes alterações no teste:

import { render, screen, fireEvent } from "@testing-library/react";
import Form from "./Form";

test("submitting the form calls onSubmit with value and coin", () => {
  const handleSubmit = jest.fn();
  render(<Form onSubmit={handleSubmit} />);

  screen.getByLabelText(/valor/i).value = "3000";
  screen.getByLabelText(/moeda/i).value = "USD";
  fireEvent.click(screen.getByRole("button", { name: /calcular/i }));
});
Enter fullscreen mode Exit fullscreen mode

Repare que o teste passa, mas uma mensagem de erro é exibida no terminal:

Error: Not implemented: HTMLFormElement.prototype.submit
Enter fullscreen mode Exit fullscreen mode

Se você abrir o arquivo jest.config.js, poderá ver a seguinte linha:

testEnvironment: "jsdom"
Enter fullscreen mode Exit fullscreen mode

O jsdom é uma implementação de vários padrões web, para uso com Node.js. Aqui é onde definimos que o Jest vai usar essa implementação, por isso temos acesso ao DOM no ambiente de teste.

O jsdom não conta com o evento submit padrão do navegador, e nós também não temos intenção de usá-lo, então para resolver isso, faça a seguinte mudança no componente:

const Form = () => {
  const handleSubmit = (e) => {
    e.preventDefault();
  };

  return (
    <form onSubmit={handleSubmit}>
Enter fullscreen mode Exit fullscreen mode

Com isso, a mensagem de erro não é mais exibida. Vamos garantir que a função handleSubmit seja chamada com as informações corretas quando o formulário é submetido.

Adicione o seguinte expect no final do arquivo Form.test.js:

expect(handleSubmit).toHaveBeenCalledWith({
  value: "3000",
  coin: "USD",
});
Enter fullscreen mode Exit fullscreen mode

Com isso o teste volta a falhar, para fazê-lo passar precisamos fazer com que o componente Form chame a função que foi passada na prop onSubmit.

Faça as seguintes alterações no arquivo Form.js:

const Form = ({ onSubmit }) => {
  const handleSubmit = (e) => {
    e.preventDefault();
    const { value, coin } = e.target.elements;

    onSubmit({
      value: value.value,
      coin: coin.value,
    });
  };
Enter fullscreen mode Exit fullscreen mode

Pronto! Com isso o teste volta a passar, você também pode adicionar mais um expect para garantir que a função foi chamada apenas uma vez:

expect(handleSubmit).toHaveBeenCalledTimes(1);
Enter fullscreen mode Exit fullscreen mode

Melhorando os eventos do usuário

Observe a forma que adicionamos valores e clicamos no botão do formulário:

screen.getByLabelText(/valor/i).value = "3000";
screen.getByLabelText(/moeda/i).value = "USD";
fireEvent.click(screen.getByRole("button", { name: /calcular/i }));
Enter fullscreen mode Exit fullscreen mode

Adicionamos alguns valores diretamente nos elementos e executamos apenas o evento de click.

Quando um usuário está preenchendo um formulário em um navegador e altera um campo, alguns eventos são disparados como keydown, keyup, change, ocorrem diferentes tipos de eventos.

Para que os testes fiquem mais próximos de como um usuário interage com a aplicação, é recomendado usar chamado user-event, que também faz parte da Testing Library.

Execute o seguinte comando para instalar o user-event:

npm i -D @testing-library/user-event
Enter fullscreen mode Exit fullscreen mode

Agora altere o arquivo Form.test.js, para que fique assim:

import { render, screen } from "@testing-library/react";
import userEvent from '@testing-library/user-event'
import Form from "./Form";

test("submitting the form calls onSubmit with value and coin", () => {
  const handleSubmit = jest.fn();
  render(<Form onSubmit={handleSubmit} />);

  userEvent.type(screen.getByLabelText(/valor/i), "3000");
  userEvent.selectOptions(screen.getByLabelText(/moeda/i), "USD");
  userEvent.click(screen.getByRole("button", { name: /calcular/i }));

  expect(handleSubmit).toHaveBeenCalledWith({
    value: "3000",
    coin: "USD",
  });
  expect(handleSubmit).toHaveBeenCalledTimes(1);
});
Enter fullscreen mode Exit fullscreen mode

Conclusão

Nesse artigo criamos um formulário, criamos um mock, passamos como prop, adicionamos eventos, enfim, fizemos bastante coisa. Confesso que foi bem difícil resumir tudo. Como queria fazer algo bem prático, não me aprofundei muito em alguns pontos, mas procurei deixar links em todos eles.

E é isso, mais uma vez espero que o conteúdo tenha ajudado e se tiver alguma dúvida ou sugestão, não deixa de comentar, isso ajuda muito :D.

E como de costume, vou deixar o link do repositório. Valeu!

Oldest comments (2)

Collapse
 
daniel0liver profile image
Daniel Oliveira

Muito top!!! Ei Felipe ali tu usou o userEvent pra criar o evento de click no botão, tem diferença em usar o fireEvent ou userEvent?

Collapse
 
felipecesr profile image
Felipe César • Edited

Valeu Daniel! Sobre o userEvent, tem diferença sim, ele se assemelha mais à forma como os usuários interagem com os elementos do DOM. Por exemplo, quando o usuário clica em um botão, alguns eventos são disparados, mousedown, mouseup, click, etc. Com o fireEvent estamos apenas disparando um evento específico. Nesse caso, poderíamos usar fireEvent.click, mas o único evento disparado seria o click, dessa forma não estaria representando a forma como o usuário interage botão.