[React + Jest] Introduzindo Testes para "Components"

dchueri

Diego Chueri

Posted on December 9, 2022

[React + Jest] Introduzindo Testes para "Components"

English version

Introdução

Diferente do Backend onde os testes unitários visam avaliar o funcionamento de um determinado método, os testes unitários para o Frontend devem focar em "simular" as possíveis ações dos usuários e como determinado componente reage a determinada ação.

Porque escrever testes?

Embora possa parecer complicado no início, testes automatizados podem evitar dor de cabeça no futuro. São a forma mais efetiva de verificar se o seu código realmente faz o que você deseja que ele faça, podem evitar que algum bug que já tenha sido corrigido previamente volte a ocorrer, e diversos outros benefícios para a sua aplicação.

Tecnologias

Para escrever nossos testes iremos utilizar a stack a seguir:

"Component" a ser testado

Para escrever um bom teste você deve conhecer bem o código e o comportamento que espera, então vamos analisar o nosso LoginForm antes de iniciar os testes:

Apresentando o Código

Como exemplo, será utilizado um component simples de um formulário de login:

import { useState } from "react";

const db = {
  email: "diego@test.com",
  password: "123456",
};

export const LoginForm = () => {
  const [email, setEmail] = useState<string>();
  const [password, setPassword] = useState<string>();
  const [logged, setLogged] = useState<Boolean>(false);
  const [error, setError] = useState<Boolean>(false);

  const login = (email: string, password: string) => {
    if (email !== db.email || password !== db.password) {
      setError(true);
      setLogged(false);
    } else {
      setError(false);
      setLogged(true);
    }
  };

  const handleSubmit = (e: React.SyntheticEvent) => {
    e.preventDefault();
    login(email!, password!);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Email
        <input
          name="email"
          type="text"
          onChange={(e) => setEmail(e.target.value)}
        />
      </label>
      <label>
        Password
        <input
          name="password"
          type="password"
          onChange={(e) => setPassword(e.target.value)}
        />
      </label>
      <button type="submit">Log in</button>
      {logged ? (
        <p>Wellcome!</p>
      ) : error ? (
        <p>Email or password invalid</p>
      ) : null}
    </form>
  );
};
Enter fullscreen mode Exit fullscreen mode

Resultando na seguinte interface:

Print screen

Entendendo o funcionamento

Foi utilizado como mock de um usuário no banco de dados a constante db:

const db = {
  email: "diego@test.com",
  password: "123456",
};
Enter fullscreen mode Exit fullscreen mode

Em seguida está o nosso LoginForm. Basicamente ele armazena o valor do input de cada campo do formulário no state por meio do onChange. Ao clicar no botão de Log In é acionado o handleSubmit que por meio do método de login faz a verificação se o e-mail e senha passados pelo usuário coincidem com a senha do usuário no banco (no nosso caso, no mock).

Caso o e-mail e senha sejam válidos, é exibido uma mensagem de boas vindas (Welcome!):

Image description

E caso algum dos dois campos possuam algum dado inválido é exibido para o usuário (Invalid email or password):

Print screen

Iniciando os testes

Vamos criar o arquivo de testes para o nosso component na mesma pasta em que ele está inserido. Por boas práticas, costumo nomear o arquivo de testes com o mesmo nome do component, seguido da extensão .spec.tsx (caso não esteja utilizando typescript como eu a extensão seria .spec.jsx).

Print screen

Iniciando, vamos utilizar o método describe, onde passaremos como primeiro parâmetro uma string que normalmente é o nome do component testado. O segundo parâmetro é uma função callback que receberá nossos testes:

describe("LoginForm", () => {})
Enter fullscreen mode Exit fullscreen mode

Os testes são escritos dentro das funções it ou test, os dois são funcionalmente idênticos, sendo que prefiro utilizar o it apenas por semântica, melhorando a legibilidade. A função também recebe dois parâmetros, onde o primeiro é a descrição do que deverá acontecer durante o teste e o segundo é mais um callback, mas dessa vez com o código do teste.

describe("LoginForm", () => {                // LoginForm...
    it("should render the login form", () => { // ...deve renderizar o formulário de login
        //code for test here
  });
})
Enter fullscreen mode Exit fullscreen mode

Neste momento podemos começar testando a renderização dos elementos.

Renderizando a UI

O método render() realizará a renderização do componente. Ele recebe como parâmetro o componente e retorna um RenderResult que tem vários métodos e propriedades utilitários.

import { render } from "@testing-library/react";
import { LoginForm } from "../LoginForm";

describe("LoginForm", () => {
  it("should render the login form", () => {
    render(<LoginForm />);
  });
})
Enter fullscreen mode Exit fullscreen mode

Capturando elementos no DOM

O objeto screen é utilizado para fazer consultas no DOM. Ele possui uma série de queries que farão a consulta no DOM conforme a necessidade e retornando o elemento escolhido.

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

describe("LoginForm", () => {
  it("should render the login form", () => {
    render(<LoginForm />);

        const emailField = screen.getByLabelText("Email");
        const passwordField = screen.getByText("Password");
        const loginButton = screen.getByRole("button", { name: "Log in" });
  });
});
Enter fullscreen mode Exit fullscreen mode

Foram apresentados três exemplos de queries dentre as diversas que podem ser utilizadas:

  • getByLabelText: Recomendado pela documentação oficial para buscar campos de formulários (no exemplo não utilizei esta query em todas as opções para ampliar a apresentação das possibilidades de uso);
  • getByText: Fora dos formulários, o conteúdo de texto é a principal forma de encontrar elementos. Este método é recomendado para localizar elementos não interativos (como divs, spans e parágrafos).
  • getByRole: Utilizado para consultar elementos pelo nome acessível dele (button, div, p, entre outros). Normalmente é utilizando com o objeto option {name: …}, evitando elementos indesejados.

Sugiro a leitura documentação com todas as queries neste link.

Hora de testar

Testando a renderização

Vamos agora finalmente testar se os elementos estão sendo renderizados. Para testar os valores é utilizada a função expect, ela utiliza funções matchers que realizam verificações entre o valor esperado e o valor recebido.

//...imports
import '@testing-library/jest-dom';

describe("LoginForm", () => {
  it("should render the login form", () => {
    render(<LoginForm />);

        const emailField = screen.getByLabelText("Email");
        const passwordField = screen.getByText("Password");
        const loginButton = screen.getByRole("button", { name: "Log in" });

        expect(emailField).toBeInTheDocument();
    expect(passwordField).toBeInTheDocument();
    expect(loginButton).toBeInTheDocument();
  });
});
Enter fullscreen mode Exit fullscreen mode

Rodando o teste com os comandos abaixo, poderemos observar o resultado no terminal.

yarn test
ou
npm run test
Enter fullscreen mode Exit fullscreen mode
PASS  src/components/LoginForm/LoginForm.spec.tsx
  LoginForm
    ✓ should render the login form (82 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.01 s, estimated 4 s
Enter fullscreen mode Exit fullscreen mode

💡 Não se esqueça de importar o módulo @testing-library/jest-dom para utilizar o mathcer toBeInDocument.

Veja mais sobre os Testing Library Custom Matchers.

Mais testes

Agora que sabemos que os elementos esperados estão sendo renderizados normalmente, iremos verificar o comportamento deles diante uma interação do usuário, e para isso utilizaremos o userEvent.

Testando o login bem sucedido

Nesse momento relembro que é necessário imaginar as ações que o usuário irá executar. Por tanto, para realizar o login o usuário primeiro insere os dados nos campos de email e password , por último ele clica no botão de Log In recebendo então a mensagem de boas vindas caso os dados estejam corretos ou, caso contrário, receberá uma mensagem de erro.

O método type emula a digitação pelo usuário, recebendo como primeiro parâmetro o elemento no qual será inserido o texto e como segundo parâmetro o texto propriamente dito. Ele retorna uma Promise portanto é necessário adicionar o await antes do userEvent e o async no início da função callback do teste.

await userEvent.type(emailField, userInput.email);
await userEvent.type(passwordField, userInput.password);
Enter fullscreen mode Exit fullscreen mode

Com o formulário preenchido, utilizaremos o método click para realizar o clique no botão de login.

await userEvent.click(loginButton);
Enter fullscreen mode Exit fullscreen mode

Agora podemos realizar a comparação entre os valores esperados e os valores recebidos:

// espera que o elemento com o texto "Welcome!" esteja no documento
expect(screen.getByText("Welcome!")).toBeInTheDocument();
// espera que o elemento com o texto "Invalid email or password" não seja encontrado, retornando null
expect(screen.queryByText("Invalid email or password")).toBeNull();
Enter fullscreen mode Exit fullscreen mode

Observe que no primeiro expect é esperado que um elemento com o texto “Welcome!” esteja no documento, utilizando o já conhecido método getByText. Já no segundo foi utilizado o queryByText no lugar de getByText, isso por conta de que é esperado que o elemento não esteja no documento, e ao utilizarmos qualquer método iniciado com get e não for encontrado nenhum elemento será lançado um erro interrompendo o teste, já os métodos iniciados com query retornam null , possibilitando a continuação do teste.

Fincando dessa forma o nosso segundo teste:

//...imports
import userEvent from "@testing-library/user-event";

describe("LoginForm", () => {
  it("should render the login form", () => {
    ...
  });

    // deve exibir uma mensagem de boas vindas caso tenha sucesso no login
  it("should show an welcome message if login success", async () => {
    render(<LoginForm />);

    const emailField = screen.getByLabelText("Email");
    const passwordField = screen.getByText("Password");
    const loginButton = screen.getByRole("button", { name: "Log in" });

    const userInput = { email: "diego@test.com", password: "123456" };

    await userEvent.type(emailField, userInput.email);
    await userEvent.type(passwordField, userInput.password);
    await userEvent.click(loginButton);

    expect(screen.getByText("Welcome!")).toBeInTheDocument();
    expect(screen.queryByText("Invalid email or password")).toBeNull();
  });
});
Enter fullscreen mode Exit fullscreen mode
PASS  src/components/LoginForm/LoginForm.spec.tsx
  LoginForm
    ✓ should render the login form (109 ms)
    ✓ should show an welcome message if login success (141 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.6 s, estimated 3 s
Enter fullscreen mode Exit fullscreen mode

Testando o login mal sucedido

O último teste que vamos escrever neste artigo é para caso o e-mail ou a senha inseridos pelo usuário sejam inválidos. A lógica será basicamente a mesma do teste anterior alternado somente a ordem das mensagens esperadas:

//...imports

describe("LoginForm", () => {
  it("should render the login form", () => {
    ...
  });

  it("should show an welcome message if login success", async () => {
    ...
  });

   // deve exibir uma mensagem de erro caso o login tenha falhado
  it("should show an error message if login failed", async () => {
    render(<LoginForm />);

    const emailField = screen.getByLabelText("Email");
    const passwordField = screen.getByText("Password");
    const loginButton = screen.getByRole("button", { name: "Log in" });

    const userInput = { email: "diego@test.com", password: "worngPassword" };

    await userEvent.type(emailField, userInput.email);
    await userEvent.type(passwordField, userInput.password);
    await userEvent.click(loginButton);

    expect(screen.getByText("Invalid email or password")).toBeInTheDocument();
    expect(screen.queryByText("Welcome!")).toBeNull();
  });
});
Enter fullscreen mode Exit fullscreen mode
PASS  src/components/LoginForm/LoginForm.spec.tsx
  LoginForm
    ✓ should render the login form (89 ms)
    ✓ should show an welcome message if login success (166 ms)
    ✓ should show an error message if login failed (126 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        2.388 s, estimated 3 s
Enter fullscreen mode Exit fullscreen mode

Conclusão

Busquei neste artigo principalmente abrir a porta para o universo dos testes. Uma ideia para exercitar o que aprendeu aqui é clonando o repositório deste projeto e criar outros testes, como por exemplo testar o que ocorrerá caso o usuário não preencha algum dos campos quando houver um required.

Com este artigo, assim como com qualquer outro artigo que você ler, busque não se restringir somente ao que foi demonstrado, utilize os links auxiliares disponibilizados ao decorrer da explicação, explore, exercite e conheça melhor cada funcionalidade para aprimorar seu conhecimento.

Repositório do Projeto

React Tests - GitHub

Leia também

Configurando o ArchLinux com WSL 2 para Devs

💖 💪 🙅 🚩
dchueri
Diego Chueri

Posted on December 9, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related