Testes Unitários - Um guia rápido de Jr para Jr

Testes Unitários - Um guia rápido de Jr para Jr

Desenvolvimento de aplicações é uma área em constante expansão nos últimos anos, sobretudo na pandemia. Devido a este intenso crescimento, a demanda de desenvolvedores de todos os níveis têm crescido muito, em descompasso com a quantidade de profissionais qualificados. Nesse cenário, faz-se necessário não apenas ser capaz de escrever código, mas garantir que o código faz o que se propõe. Atestar essa qualidade. É em função disso que fiz esse guia, para apresentar algumas formas de fazer testes unitários.

A estrutura de teste

Para trazer o conteúdo de forma prática optei por utilizar a biblioteca Jest em exemplos. Minha escolha se deve ao fato de ser uma das ferramentas mais utilizadas na construção de testes unitários atualmente, à sua popularidade e ao alto percentual de adesão a frameworks baseados em Javascript.

Jest é uma biblioteca que oferece um ambiente no qual podemos desenvolver formas de verificar que determinado código em Javascript está tendo o comportamento esperado dentro do sistema em questão. A estrutura base consiste em 3 partes:

  • uma descrição da unidade que está sendo testada;

  • uma descrição da expectativa geral de funcionamento de um bloco; e por fim,

  • estruturamos as expectativas no decorrer do processamento do código.

Podemos observar cada parte no exemplo abaixo, respectivamente describe(), it(), expect().

describe("UNIT DESCRIPTION", () => {
    it("block expectation description", () => {
        expect().toBeValid();
    });
});

Testando Funções

Com essas estruturas e seus usos em mente, trago um exemplo de como ficaria o teste de uma função feita em Javascript. A função em questão recebe um valor de email e o testa num padrão RegEx, retornando um booleano. Segue a função:

export const emailValidation = (value) => {
  const pattern = /^([a-z\d.-]+)@([a-z\d-]+)\.([a-z]{2,8})(\.[a-z]{2,8})?$/;

  return pattern.test(value);
};

Nesse caso, o funcionamento correto da função é retornar verdadeiro caso o parâmetro seja um email válido, e falso caso contrário. Seguindo essa linha de pensamento podemos verificar os valores de entrada e saída da seguinte forma:

describe("emailValidation", () => {
    it("should return true if email is valid", () => {
        const email = "valid@email.com";
        expect(emailValidation(email)).toBeTruthy();
    });

    it("should return false if email is invalid", () => {
        const email = "invalid/email.co@";
        expect(emailValidation(email)).toBeFalsy();
    });
});

No teste acima faço verificação das duas possibilidades, passando e-mails válidos e inválidos. O teste pode e deve cobrir a maior quantidade dentre as mais variadas possibilidades(undefined, null, string vazias, etc…), mas por hora observe a estrutura da expectativa nos diferentes casos. A lista de expectativas são predefinidas, mas com a variedade disponível não é comum ter dificuldade para suprir os casos de uso.

Testando Elementos no DOM

Mesmo sendo bastante completa, a biblioteca Jest pode ainda ser utilizada combinada com outras ferramentas para estender a cobertura de testes. O caso anterior verificava a lógica de funcionamento de uma função, mas há outras situações como por exemplo a verificação de elementos no DOM, que veremos a seguir. Abaixo temos um componente React Js, uma popup para fazer login:

import LogoImage from "../assets/images/logo.svg";
import { emailValidation } from "../utils/validation.js";

const LoginPopup = ({ loginFunction }) => {
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");

    const didLogin = () => {
        if (emailValidation(email)) {
            loginFunction(email, password);
        }
    };

    return (
        <div>
            <label htmlFor="email">
                <h5>Email</h5>
            </label>
            <input type="text" onChange={(e) => setEmail(e.target.value)} value={email} id="email" />
            <label htmlFor="password">
                <h5>Password</h5>
            </label>
            <input type="password" onChange={(e) => setPassword(e.target.value)} value={password} id="password" />
            <button onClick={() => didLogin()}>Confirmar</button>

            <img
                src={LogoImage}
                style={{
                    margin: "10",
                    width: "40",
                    height: "40",
                    objectFit: "cover",
                }}
                alt="Logo"
            />
        </div>
    );
};

export default LoginPopup;

Para testar esse componente nos próximos exemplos faço uso de ferramentas de outra biblioteca de testes, nesse caso a ferramenta render() da Testing Library para React. Esta ferramenta serve para gerar uma renderização de avaliação dos elementos presentes nos componentes renderizados. E é aplicada da seguinte forma:

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

import LoginPopup from "./LoginPopup";

describe("LoginPopup", () => {
    it("should have a logo with specified style.", () => {
        const { getByAltText } = getRenderer();

        expect(getByAltText("Logo")).toHaveStyle({
            margin: "10",
            width: "40",
            height: "40",
            objectFit: "cover",
        });
    });
});

function getRenderer() {
    return render(<LoginPopup />);
}

Tendo sido chamado a função render() conforme documentação da Testing Library(TL), podemos observar as 3 estruturas do Jest mantidas:

  • a unidade em teste - LoginPopup;

  • a descrição do teste - nesse caso verificando a existência de uma imagem logo a ser renderizada com uma estilização específica; e,

  • as expectativas que dão por confirmado o funcionamento correto, usando a biblioteca adicional(TL) para capturar o objeto alvo e o Jest para verificar os estilos.

No exemplo a seguir eu demonstro uma forma muito útil na hora de fazer testes repetidos. A estrutura .each([ ]), que serve para encadear testes iguais. Neste caso, uso para verificar que os headings “Email” e “Password” estão no DOM.

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

import LoginPopup from "./LoginPopup";

describe("<LoginPopup />", () => {
    it.each(["Email", "Password"])("should have a heading '%s'.", (expected) => {
        const { getByRole } = getRenderer();

        expect(getByRole("heading", {name: expected})).toBeInTheDocument();
    });
});

function getRenderer() {
    return render(<LoginPopup />);
}

Os termos dentro dessa estrutura, que podem ser únicos ou um array, são passados como parâmetro e são esperados na propriedade name dos títulos (tags h1,h2,h5...) e são representados através da variável expected. É válido explicitar que cada item dentro de .each([ ]) irá disparar um teste sob as mesmas condições e expectativas então cuidado na hora de avaliar quais testes você pode encadear.

Testando Interações no DOM

Por último apresento um exemplo um pouco mais elaborado com o uso da ferramenta userEvent, também da Testing Library, que simula as interações que um usuário possa ter com a tela em apresentação.

Neste caso, meu objetivo é verificar que determinada função de login é chamada quando o usuário digita um email válido e clica no botão “Confirmar”. O que deveria em seguida logar o usuário e disparar uma mudança na tela renderizada.

import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import LoginPopup from "./LoginPopup";

describe("<LoginPopup />", () => {
    it("should log user in and change rendering if typed valid email and clicked in button.", (expected) => {
        const signInWithEmail = jest.fn()

        const { getByRole, queryByRole } = getRenderer({signInWithEmail});

        userEvent.type(getByRole("textbox"), "valid@email.com")
        userEvent.click(getByRole("button"))

        expect(signInWithEmail).toBeCalledTimes(1);
        expect(queryByRole("textbox")).not.toBeInTheDocument();
    });
});

function getRenderer({signInWithEmail}) {
    return render(<LoginPopup loginFunction={signInWithEmail} />);
}

Para isto usei o mock de uma função gerada pelo Jest(jest.fn()) e passo para o componente como propriedade. Repare o percurso de passagem para o renderizador e depois para o componente. Depois o uso de ambos eventos digitação e clique. Aqui chamo atenção para a possibilidade de tornar a expectativa negativa com o .not(na última linha do bloco de teste). Note também que nesse teste foi preciso cumprir duas expectativas, uma quanto ao mock da função ser chamada corretamente e a segunda é sobre o campo de digitação não estar mais sendo renderizado no documento revelando uma mudança de tela.

É dessa forma que a lógica dos testes vai se construindo e conforme os testes vão crescendo, há a necessidade de refatorar ou remover testes mais antigos que possam estar sendo cobertos por testes mais atuais. Em conclusão, ressalto a importância de testes como esses. Se você ficou com alguma dúvida ou tem algum comentário, não hesite em compartilhar. ;)