Javascript

Testes automatizados com Jest Já leu

13 min de leitura

Testes automatizados com Jest
Existe um momento na vida de todo desenvolvedor em que ele mexe em um código que "estava funcionando" e quebra algo que nem sabia que existia. A solução para isso não é ter mais cuidado — é ter testes automatizados. Test

Existe um momento na vida de todo desenvolvedor em que ele mexe em um código que "estava funcionando" e quebra algo que nem sabia que existia. A solução para isso não é ter mais cuidado — é ter testes automatizados.

Testes não são burocracia. São a rede de segurança que te permite refatorar com confiança, adicionar funcionalidades sem medo e documentar o comportamento esperado do código de forma executável.

O Jest é o framework de testes mais popular do ecossistema JavaScript — funciona para Node.js, React, TypeScript e praticamente qualquer projeto JS moderno.


Por que testar?

Sem testes                         Com testes
──────────────────────────────     ──────────────────────────────────
"Funciona na minha máquina"        "Funciona em qualquer máquina"
Medo de refatorar                  Refatoração com confiança
Bugs descobertos em produção       Bugs descobertos antes do deploy
Documentação desatualizada         Testes são documentação viva
Dependência de testes manuais      Feedback em segundos

Tipos de teste

┌──────────────────────────────────────────────────────┐
│                    PIRÂMIDE DE TESTES                │
│                                                      │
│                        /                            │
│                       /E2E         Poucos, lentos   │
│                      /──────       (Cypress, etc.)  │
│                     /Integra-                       │
│                    / ção           Médios            │
│                   /────────────                     │
│                  /                                  │
│                 /  Unitários       Muitos, rápidos  │
│                /──────────────────                  │
└──────────────────────────────────────────────────────┘

Unitários    → Testam uma função isolada
Integração   → Testam módulos funcionando juntos
E2E          → Testam o sistema completo como o usuário usa

Neste artigo focamos em testes unitários e de integração com Jest.


Instalando e configurando

npm install -D jest

# Para projetos com ES Modules ou TypeScript:
npm install -D @babel/core @babel/preset-env babel-jest
// package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  },
  "jest": {
    "testEnvironment": "node",
    "coverageDirectory": "coverage",
    "collectCoverageFrom": ["src/**/*.js"],
    "testPathPattern": ["tests/", "\.test\.js$"]
  }
}

A anatomia de um teste

// soma.js
function soma(a, b) {
  return a + b;
}
module.exports = { soma };

// soma.test.js
const { soma } = require("./soma");

// describe — agrupa testes relacionados
describe("soma()", () => {

  // it / test — um caso de teste
  it("soma dois números positivos", () => {
    // Arrange — prepara
    const a = 3;
    const b = 4;

    // Act — executa
    const resultado = soma(a, b);

    // Assert — verifica
    expect(resultado).toBe(7);
  });

  it("soma números negativos", () => {
    expect(soma(-2, -3)).toBe(-5);
  });

  it("soma zero", () => {
    expect(soma(5, 0)).toBe(5);
  });
});
npx jest soma.test.js

# PASS soma.test.js
#   soma()
#     ✓ soma dois números positivos (2ms)
#     ✓ soma números negativos
#     ✓ soma zero
#
# Test Suites: 1 passed
# Tests:       3 passed

Matchers — o vocabulário dos assertions

// ── Igualdade ──────────────────────────────────────
expect(2 + 2).toBe(4);              // ===  (primitivos)
expect({ a: 1 }).toEqual({ a: 1 }); // igualdade profunda (objetos)
expect({ a: 1 }).not.toBe({ a: 1 });// referências diferentes

// ── Verdade / Falsidade ────────────────────────────
expect(true).toBeTruthy();
expect(0).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect("texto").toBeDefined();

// ── Números ────────────────────────────────────────
expect(10).toBeGreaterThan(5);
expect(10).toBeGreaterThanOrEqual(10);
expect(3).toBeLessThan(5);
expect(3.14).toBeCloseTo(3.1415, 1); // casas decimais

// ── Strings ────────────────────────────────────────
expect("JavaScript").toContain("Script");
expect("hello@email.com").toMatch(/^S+@S+.S+$/);
expect("Node.js").toMatch("Node");

// ── Arrays ─────────────────────────────────────────
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);
expect([1, 2, 3]).toEqual(expect.arrayContaining([1, 3]));

// ── Objetos ────────────────────────────────────────
expect({ nome: "Ana", idade: 28 }).toMatchObject({ nome: "Ana" });
expect({ a: 1 }).toHaveProperty("a");
expect({ a: 1 }).toHaveProperty("a", 1);

// ── Erros ──────────────────────────────────────────
expect(() => JSON.parse("inválido")).toThrow();
expect(() => JSON.parse("inválido")).toThrow(SyntaxError);
expect(() => { throw new Error("ops") }).toThrow("ops");

// ── Async ──────────────────────────────────────────
await expect(Promise.resolve(42)).resolves.toBe(42);
await expect(Promise.reject(new Error("falhou"))).rejects.toThrow("falhou");

Testando funções puras

As mais fáceis de testar — sem dependências externas:

// src/utils/validacoes.js
function validarEmail(email) {
  if (!email || typeof email !== "string") return false;
  return /^S+@S+.S+$/.test(email.trim());
}

function validarCPF(cpf) {
  const limpo = cpf.replace(/D/g, "");
  if (limpo.length !== 11) return false;
  if (/^(d)1{10}$/.test(limpo)) return false; // todos iguais

  // Algoritmo de validação do CPF
  let soma = 0;
  for (let i = 0; i < 9; i++) soma += Number(limpo[i]) * (10 - i);
  let dig1 = 11 - (soma % 11);
  if (dig1 >= 10) dig1 = 0;
  if (dig1 !== Number(limpo[9])) return false;

  soma = 0;
  for (let i = 0; i < 10; i++) soma += Number(limpo[i]) * (11 - i);
  let dig2 = 11 - (soma % 11);
  if (dig2 >= 10) dig2 = 0;
  return dig2 === Number(limpo[10]);
}

function calcularIdade(dataNascimento) {
  const hoje = new Date();
  const nasc = new Date(dataNascimento);
  let idade = hoje.getFullYear() - nasc.getFullYear();
  const mes = hoje.getMonth() - nasc.getMonth();
  if (mes < 0 || (mes === 0 && hoje.getDate() < nasc.getDate())) {
    idade--;
  }
  return idade;
}

module.exports = { validarEmail, validarCPF, calcularIdade };
// tests/utils/validacoes.test.js
const { validarEmail, validarCPF, calcularIdade } = require("../../src/utils/validacoes");

describe("validarEmail()", () => {
  describe("emails válidos", () => {
    it.each([
      "usuario@email.com",
      "nome.sobrenome@empresa.com.br",
      "user+tag@domain.org",
      "123@numbers.com",
    ])("aceita %s", (email) => {
      expect(validarEmail(email)).toBe(true);
    });
  });

  describe("emails inválidos", () => {
    it.each([
      ["sem arroba", "semArroba.com"],
      ["sem domínio", "usuario@"],
      ["vazio", ""],
      ["null", null],
      ["número", 123],
    ])("%s: %s", (_, email) => {
      expect(validarEmail(email)).toBe(false);
    });
  });
});

describe("validarCPF()", () => {
  it("valida CPF válido sem formatação", () => {
    expect(validarCPF("529.982.247-25")).toBe(true);
  });

  it("valida CPF válido com formatação", () => {
    expect(validarCPF("52998224725")).toBe(true);
  });

  it("rejeita CPF com dígitos repetidos", () => {
    expect(validarCPF("111.111.111-11")).toBe(false);
    expect(validarCPF("00000000000")).toBe(false);
  });

  it("rejeita CPF com tamanho errado", () => {
    expect(validarCPF("123")).toBe(false);
    expect(validarCPF("")).toBe(false);
  });

  it("rejeita CPF com dígito verificador errado", () => {
    expect(validarCPF("529.982.247-26")).toBe(false);
  });
});

describe("calcularIdade()", () => {
  it("calcula a idade corretamente", () => {
    const hoje = new Date();
    const anoNasc = hoje.getFullYear() - 25;
    const dataNasc = `${anoNasc}-${String(hoje.getMonth() + 1).padStart(2, "0")}-01`;
    expect(calcularIdade(dataNasc)).toBe(25);
  });

  it("considera aniversário que ainda não ocorreu este ano", () => {
    const hoje = new Date();
    const anoNasc = hoje.getFullYear() - 30;
    // Usa mês futuro para garantir que o aniversário não ocorreu
    const mesFuturo = ((hoje.getMonth() + 2) % 12) + 1;
    const dataNasc = `${anoNasc}-${String(mesFuturo).padStart(2, "0")}-01`;
    expect(calcularIdade(dataNasc)).toBe(29);
  });
});

Mocks — isolando dependências

Mocks substituem dependências reais (banco de dados, APIs externas, sistema de arquivos) por versões controladas:

// src/services/emailService.js
const nodemailer = require("nodemailer");

async function enviarEmail({ para, assunto, corpo }) {
  const transporter = nodemailer.createTransport({
    host: process.env.EMAIL_HOST,
    port: 587,
    auth: {
      user: process.env.EMAIL_USER,
      pass: process.env.EMAIL_PASS,
    },
  });

  await transporter.sendMail({
    from: process.env.EMAIL_USER,
    to: para,
    subject: assunto,
    html: corpo,
  });
}

module.exports = { enviarEmail };
// tests/services/emailService.test.js
const nodemailer = require("nodemailer");

// Mock do módulo inteiro
jest.mock("nodemailer");

describe("enviarEmail()", () => {
  let sendMailMock;

  beforeEach(() => {
    // Configura o mock antes de cada teste
    sendMailMock = jest.fn().mockResolvedValue({ messageId: "abc123" });
    nodemailer.createTransport.mockReturnValue({ sendMail: sendMailMock });
  });

  afterEach(() => {
    jest.clearAllMocks(); // limpa chamadas registradas
  });

  it("chama sendMail com os dados corretos", async () => {
    const { enviarEmail } = require("../../src/services/emailService");

    await enviarEmail({
      para: "ana@email.com",
      assunto: "Bem-vinda!",
      corpo: "<h1>Olá Ana</h1>",
    });

    expect(sendMailMock).toHaveBeenCalledTimes(1);
    expect(sendMailMock).toHaveBeenCalledWith(
      expect.objectContaining({
        to: "ana@email.com",
        subject: "Bem-vinda!",
        html: "<h1>Olá Ana</h1>",
      })
    );
  });

  it("propaga erros do transporter", async () => {
    sendMailMock.mockRejectedValue(new Error("SMTP indisponível"));
    const { enviarEmail } = require("../../src/services/emailService");

    await expect(
      enviarEmail({ para: "x@x.com", assunto: "Teste", corpo: "" })
    ).rejects.toThrow("SMTP indisponível");
  });
});

jest.fn() — funções mock

// Criar uma função mock
const minhaFn = jest.fn();

// Definir retorno
const soma = jest.fn().mockReturnValue(10);
const buscar = jest.fn().mockResolvedValue({ id: 1, nome: "Ana" });
const falhar = jest.fn().mockRejectedValue(new Error("falhou"));

// Retornos diferentes em chamadas sequenciais
const iterador = jest.fn()
  .mockReturnValueOnce("primeiro")
  .mockReturnValueOnce("segundo")
  .mockReturnValue("padrão");

// Inspecionar chamadas
minhaFn("arg1", "arg2");
expect(minhaFn).toHaveBeenCalled();
expect(minhaFn).toHaveBeenCalledTimes(1);
expect(minhaFn).toHaveBeenCalledWith("arg1", "arg2");
expect(minhaFn).toHaveBeenLastCalledWith("arg1", "arg2");

// Acessar chamadas diretamente
console.log(minhaFn.mock.calls);     // [[arg1, arg2], ...]
console.log(minhaFn.mock.results);   // [{ type: "return", value: ... }]

Hooks de ciclo de vida

describe("Suite de testes", () => {

  // Executa uma vez antes de todos os testes do describe
  beforeAll(async () => {
    await conectarBancoDeTeste();
  });

  // Executa uma vez após todos os testes do describe
  afterAll(async () => {
    await desconectarBancoDeTeste();
  });

  // Executa antes de CADA teste
  beforeEach(() => {
    jest.clearAllMocks();
    estadoInicial = criarEstadoInicial();
  });

  // Executa após CADA teste
  afterEach(async () => {
    await limparBancoDeTeste();
  });

  it("teste 1", () => { /* ... */ });
  it("teste 2", () => { /* ... */ });
});

Testando a API com Supertest

O Supertest permite testar rotas HTTP sem subir um servidor real:

npm install -D supertest
// tests/integration/auth.test.js
const request = require("supertest");
const mongoose = require("mongoose");
const app = require("../../src/index");
const Usuario = require("../../src/models/Usuario");

// Conecta ao banco de teste antes de tudo
beforeAll(async () => {
  await mongoose.connect(process.env.MONGODB_TEST_URL ||
    "mongodb://localhost:27017/tarefas-test");
});

// Limpa a coleção antes de cada teste
beforeEach(async () => {
  await Usuario.deleteMany({});
});

// Desconecta após todos os testes
afterAll(async () => {
  await mongoose.connection.close();
});

describe("POST /auth/registrar", () => {
  const dadosValidos = {
    nome: "Ana Paula",
    email: "ana@email.com",
    senha: "senha123",
  };

  it("registra um novo usuário com sucesso", async () => {
    const res = await request(app)
      .post("/auth/registrar")
      .send(dadosValidos);

    expect(res.status).toBe(201);
    expect(res.body).toHaveProperty("token");
    expect(res.body.usuario).toMatchObject({
      nome: "Ana Paula",
      email: "ana@email.com",
    });
    expect(res.body.usuario).not.toHaveProperty("senha");
  });

  it("rejeita email duplicado", async () => {
    await request(app).post("/auth/registrar").send(dadosValidos);

    const res = await request(app)
      .post("/auth/registrar")
      .send(dadosValidos);

    expect(res.status).toBe(409);
    expect(res.body.erro).toMatch(/já cadastrado/i);
  });

  it("rejeita nome ausente", async () => {
    const res = await request(app)
      .post("/auth/registrar")
      .send({ email: "ana@email.com", senha: "123456" });

    expect(res.status).toBe(422);
  });

  it("rejeita senha curta", async () => {
    const res = await request(app)
      .post("/auth/registrar")
      .send({ nome: "Ana", email: "ana@email.com", senha: "123" });

    expect(res.status).toBe(422);
  });
});

describe("POST /auth/login", () => {
  beforeEach(async () => {
    await request(app).post("/auth/registrar").send({
      nome: "Ana Paula",
      email: "ana@email.com",
      senha: "senha123",
    });
  });

  it("faz login com credenciais corretas", async () => {
    const res = await request(app)
      .post("/auth/login")
      .send({ email: "ana@email.com", senha: "senha123" });

    expect(res.status).toBe(200);
    expect(res.body).toHaveProperty("token");
    expect(typeof res.body.token).toBe("string");
  });

  it("rejeita senha incorreta", async () => {
    const res = await request(app)
      .post("/auth/login")
      .send({ email: "ana@email.com", senha: "errada" });

    expect(res.status).toBe(401);
    expect(res.body.erro).toMatch(/credenciais inválidas/i);
  });

  it("rejeita email não cadastrado", async () => {
    const res = await request(app)
      .post("/auth/login")
      .send({ email: "naoexiste@email.com", senha: "senha123" });

    expect(res.status).toBe(401);
  });
});

describe("GET /tarefas (rota protegida)", () => {
  it("rejeita requisição sem token", async () => {
    const res = await request(app).get("/tarefas");
    expect(res.status).toBe(401);
  });

  it("aceita requisição com token válido", async () => {
    // Registra e faz login para obter o token
    const loginRes = await request(app)
      .post("/auth/login")
      .send({ email: "ana@email.com", senha: "senha123" });

    // Usa o token na requisição protegida
    const res = await request(app)
      .get("/tarefas")
      .set("Authorization", `Bearer ${loginRes.body.token}`);

    expect(res.status).toBe(200);
    expect(res.body).toHaveProperty("dados");
    expect(Array.isArray(res.body.dados)).toBe(true);
  });
});

Cobertura de código

npm run test:coverage

# ----------------------|---------|----------|---------|---------|
# File                  | % Stmts | % Branch | % Funcs | % Lines |
# ----------------------|---------|----------|---------|---------|
# All files             |   87.5  |    82.3  |   91.2  |   87.1  |
#  controllers/         |         |          |         |         |
#   authController.js   |   92.3  |    88.9  |   100   |   92.0  |
#   tarefaController.js |   84.2  |    77.8  |   85.7  |   83.9  |
#  utils/               |         |          |         |         |
#   validacoes.js        |   100   |    100   |   100   |   100   |
# ----------------------|---------|----------|---------|---------|
// package.json — definir meta de cobertura mínima
{
  "jest": {
    "coverageThreshold": {
      "global": {
        "branches": 70,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    }
  }
}

Boas práticas com Jest

// ✅ 1. Nomeie testes de forma descritiva
// ❌ it("funciona", ...)
// ✅ it("retorna 404 quando o usuário não existe", ...)

// ✅ 2. Um assert por teste (idealmente)
// Facilita identificar exatamente o que falhou

// ✅ 3. Use describe para agrupar casos relacionados
// Cada arquivo de teste → um módulo
// Cada describe → um comportamento
// Cada it → um cenário

// ✅ 4. Siga o padrão AAA
// Arrange — Prepara os dados
// Act     — Executa a função
// Assert  — Verifica o resultado

// ✅ 5. Teste os casos de borda
// Valores nulos, vazios, negativos, limites
// Não apenas o "caminho feliz"

// ✅ 6. Mantenha testes rápidos e independentes
// Cada teste deve poder rodar isoladamente
// Sem dependência de ordem de execução

// ✅ 7. Use factories para criar dados de teste
function criarUsuario(overrides = {}) {
  return {
    nome: "Ana Paula",
    email: "ana@email.com",
    senha: "senha123",
    ...overrides,
  };
}

// ✅ 8. Não teste implementação — teste comportamento
// ❌ Verificar se uma variável interna mudou
// ✅ Verificar o resultado retornado ou o efeito colateral

Tarefa para você

Escreva testes para o sistema de tarefas do artigo anterior:

// 1. Testes unitários para uma função utilitária:
//    gerarSlug("Meu Post Incrível!") → "meu-post-incrivel"
//    Casos: acentos, caracteres especiais, múltiplos espaços, vazio

// 2. Testes de integração para POST /tarefas:
//    - Cria tarefa com sucesso (201)
//    - Rejeita tarefa sem título (422)
//    - Rejeita requisição sem token (401)
//    - Cria tarefa com prazo e verifica campo "atrasada"

// 3. Testes para GET /tarefas:
//    - Retorna apenas tarefas do usuário logado
//    - Filtro por status funciona
//    - Paginação funciona (pagina=1&por_pagina=2)
//    - Usuário A não vê tarefas do Usuário B

// 4. Mock de serviço externo:
//    Simule um serviço de notificação que é chamado
//    quando uma tarefa é criada. Verifique que:
//    - Foi chamado uma vez
//    - Recebeu o ID da tarefa criada
//    - Se falhar, a tarefa ainda é criada (log de erro)

Conclusão

Neste artigo você aprendeu:

  • Por que testes automatizados são essenciais em projetos profissionais
  • Os três tipos de teste — unitários, integração e E2E
  • Como instalar e configurar o Jest
  • A anatomia de um teste — describe, it, expect
  • Os matchers mais importantes do Jest
  • Como testar funções puras com it.each para múltiplos casos
  • Mocks — jest.mock(), jest.fn() e como controlar dependências
  • Os hooks de ciclo de vida — beforeAll, afterAll, beforeEach, afterEach
  • Como testar APIs REST com Supertest
  • Cobertura de código e como definir metas mínimas
  • Boas práticas que fazem a diferença no dia a dia

No próximo artigo vamos aprender sobre ESLint e Prettier — as ferramentas que mantêm o código padronizado e livre de erros comuns em times.


 

📚 Fontes e Referências

  • Jest — Documentação oficial: https://jestjs.io/pt-BR/docs/getting-started
  • Jest — Matchers: https://jestjs.io/pt-BR/docs/expect
  • Jest — Mock Functions: https://jestjs.io/pt-BR/docs/mock-functions
  • Supertest: https://github.com/ladjs/supertest
  • JavaScript.info — Testing: https://javascript.info/testing-mocha
  • The Art of Unit Testing — Roy Osherove (Manning)
  • Testing JavaScript Applications — Lucas da Costa (Manning)
  • Kent C. Dodds — Testing Trophy: https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications
  • roadmap.sh — Testing: https://roadmap.sh/testing
Comentários

Mais em Javascript

Revisão  + Projeto Final: Produção Real
Revisão + Projeto Final: Produção Real

Chegamos ao fim de mais um módulo. Em três artigos cobrimos deploy, segurança...

Git avançado e fluxo de trabalho em equipe
Git avançado e fluxo de trabalho em equipe

Saber usar git add, git commit e git push é o mínimo. Em um projeto real com...

Introdução ao Node.js: JavaScript fora do navegador
Introdução ao Node.js: JavaScript fora do navegador

Até agora todo o JavaScript que escrevemos rodou no navegador. O navegador é...