Vamos escrever testes para REST API

Iniciante | 90 min leitura | 2025.12.20

O que voce vai aprender neste tutorial

  • Configuracao do Jest e Supertest
  • Testes de endpoints GET
  • Testes de endpoints POST
  • Testes de APIs que requerem autenticacao
  • Mocks de banco de dados
  • Melhores praticas de testes

Pre-requisitos: Node.js 18 ou superior, npm/yarn/pnpm instalados. Conhecimento basico de Express facilitara o entendimento.

O que e teste de software? Por que e necessario?

Historia dos testes

O conceito de teste de software remonta aos anos 1950, mas o Desenvolvimento Orientado a Testes (TDD) moderno foi sistematizado por Kent Beck no final dos anos 1990.

“Testes nao medem qualidade. Testes constroem qualidade.” — Kent Beck

Por que escrever testes

  1. Prevencao de regressao: Detectar precocemente quebras em funcionalidades existentes
  2. Documentacao: Testes mostram como usar o codigo
  3. Confianca para refatoracao: Com testes, pode-se melhorar o codigo com seguranca
  4. Melhoria do design: Codigo testavel tende a ter um bom design

Piramide de testes

A “Piramide de Testes” proposta por Martin Fowler e um modelo que mostra os tipos e o equilibrio dos testes:

flowchart TB
    subgraph Pyramid["Piramide de Testes"]
        direction TB
        E2E["Testes E2E (poucos)<br/>Automacao de navegador"]
        Integration["Testes de Integracao (moderado)<br/>Testes de API, Testes de componentes"]
        Unit["Testes Unitarios (muitos)<br/>Funcoes, Classes"]
    end

    E2E --> Integration --> Unit
TipoVelocidadeCusto de ManutencaoConfiabilidadeCobertura
E2ELentoAltoFragilAmpla
IntegracaoModeradoModeradoModeradoModerada
UnitarioRapidoBaixoEstavelEstreita

Referencia: Martin Fowler - Test Pyramid

Posicionamento dos testes de API

Testes de API sao classificados como “testes de integracao” e tem as seguintes vantagens:

  • Nao sao afetados por mudancas na UI
  • Mais rapidos e estaveis que E2E
  • Testam requisicoes HTTP reais
  • Permitem testar o backend sem frontend

Step 1: Configuracao do projeto

Primeiro, vamos criar uma aplicacao Express simples para testar.

Criacao do projeto

mkdir api-testing-tutorial
cd api-testing-tutorial
npm init -y
npm install express
npm install -D jest supertest @types/jest @types/supertest typescript ts-jest

package.json (secao scripts)

{
  "scripts": {
    "start": "node dist/index.js",
    "build": "tsc",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

Configuracao do Jest

jest.config.js

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/*.test.ts'],
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.test.ts'
  ],
  // Setup antes e depois dos testes
  setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],
  // Limite de cobertura (opcional)
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
};

Documentacao oficial: Jest Configuration

Step 2: Criacao da API a ser testada

src/app.ts

import express, { Express, Request, Response, NextFunction } from 'express';

const app: Express = express();
app.use(express.json());

// Armazenamento de dados em memoria
interface User {
  id: number;
  name: string;
  email: string;
}

let users: User[] = [
  { id: 1, name: '田中太郎', email: 'tanaka@example.com' },
  { id: 2, name: '佐藤花子', email: 'sato@example.com' }
];

// Middleware de tratamento de erros
const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal Server Error' });
};

// GET /api/users - Lista de usuarios
app.get('/api/users', (req: Request, res: Response) => {
  res.json(users);
});

// GET /api/users/:id - Detalhes do usuario
app.get('/api/users/:id', (req: Request, res: Response) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    return res.status(404).json({ error: 'Usuario nao encontrado' });
  }
  res.json(user);
});

// POST /api/users - Criacao de usuario
app.post('/api/users', (req: Request, res: Response) => {
  const { name, email } = req.body;

  // Validacao
  if (!name || !email) {
    return res.status(400).json({ error: 'Nome e email sao obrigatorios' });
  }

  if (!email.includes('@')) {
    return res.status(400).json({ error: 'Formato de email invalido' });
  }

  const newUser: User = {
    id: users.length + 1,
    name,
    email
  };
  users.push(newUser);

  res.status(201).json(newUser);
});

// PUT /api/users/:id - Atualizacao de usuario
app.put('/api/users/:id', (req: Request, res: Response) => {
  const userId = parseInt(req.params.id);
  const userIndex = users.findIndex(u => u.id === userId);

  if (userIndex === -1) {
    return res.status(404).json({ error: 'Usuario nao encontrado' });
  }

  const { name, email } = req.body;
  users[userIndex] = { ...users[userIndex], name, email };

  res.json(users[userIndex]);
});

// DELETE /api/users/:id - Exclusao de usuario
app.delete('/api/users/:id', (req: Request, res: Response) => {
  const userId = parseInt(req.params.id);
  const userIndex = users.findIndex(u => u.id === userId);

  if (userIndex === -1) {
    return res.status(404).json({ error: 'Usuario nao encontrado' });
  }

  users.splice(userIndex, 1);
  res.status(204).send();
});

app.use(errorHandler);

// Funcao para resetar dados para testes
export const resetUsers = () => {
  users = [
    { id: 1, name: '田中太郎', email: 'tanaka@example.com' },
    { id: 2, name: '佐藤花子', email: 'sato@example.com' }
  ];
};

export default app;

Step 3: Testes basicos de GET

src/app.test.ts

import request from 'supertest';
import app, { resetUsers } from './app';

describe('Users API', () => {
  // Reset dos dados antes de cada teste
  beforeEach(() => {
    resetUsers();
  });

  // Testes de GET /api/users
  describe('GET /api/users', () => {
    it('should return all users', async () => {
      const response = await request(app)
        .get('/api/users')
        .expect('Content-Type', /json/)
        .expect(200);

      expect(response.body).toBeInstanceOf(Array);
      expect(response.body.length).toBeGreaterThan(0);
    });

    it('should return users with correct properties', async () => {
      const response = await request(app)
        .get('/api/users')
        .expect(200);

      const user = response.body[0];
      expect(user).toHaveProperty('id');
      expect(user).toHaveProperty('name');
      expect(user).toHaveProperty('email');
    });
  });

  // Testes de GET /api/users/:id
  describe('GET /api/users/:id', () => {
    it('should return a user by id', async () => {
      const response = await request(app)
        .get('/api/users/1')
        .expect(200);

      expect(response.body.id).toBe(1);
      expect(response.body).toHaveProperty('name');
    });

    it('should return 404 for non-existent user', async () => {
      const response = await request(app)
        .get('/api/users/999')
        .expect(404);

      expect(response.body.error).toBe('Usuario nao encontrado');
    });

    it('should handle invalid id format', async () => {
      const response = await request(app)
        .get('/api/users/invalid')
        .expect(404);
    });
  });
});

Executando os testes

npm test

# Saida de exemplo
# PASS  src/app.test.ts
#   Users API
#     GET /api/users
#       ✓ should return all users (25 ms)
#       ✓ should return users with correct properties (8 ms)
#     GET /api/users/:id
#       ✓ should return a user by id (5 ms)
#       ✓ should return 404 for non-existent user (4 ms)

Step 4: Testes de POST

Adicionando testes de criacao de dados.

describe('POST /api/users', () => {
  it('should create a new user', async () => {
    const newUser = {
      name: '山田次郎',
      email: 'yamada@example.com'
    };

    const response = await request(app)
      .post('/api/users')
      .send(newUser)
      .expect('Content-Type', /json/)
      .expect(201);

    expect(response.body).toMatchObject(newUser);
    expect(response.body).toHaveProperty('id');
  });

  it('should return 400 when name is missing', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({ email: 'test@example.com' })
      .expect(400);

    expect(response.body.error).toContain('obrigatorios');
  });

  it('should return 400 when email is missing', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({ name: 'Usuario Teste' })
      .expect(400);

    expect(response.body.error).toContain('obrigatorios');
  });

  it('should return 400 for invalid email format', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({ name: 'Usuario Teste', email: 'invalid-email' })
      .expect(400);

    expect(response.body.error).toContain('formato');
  });
});

Step 5: Testes de PUT/DELETE

describe('PUT /api/users/:id', () => {
  it('should update an existing user', async () => {
    const updatedData = {
      name: '田中太郎 (Atualizado)',
      email: 'tanaka-updated@example.com'
    };

    const response = await request(app)
      .put('/api/users/1')
      .send(updatedData)
      .expect(200);

    expect(response.body.name).toBe(updatedData.name);
    expect(response.body.email).toBe(updatedData.email);
  });

  it('should return 404 for non-existent user', async () => {
    await request(app)
      .put('/api/users/999')
      .send({ name: 'test', email: 'test@example.com' })
      .expect(404);
  });
});

describe('DELETE /api/users/:id', () => {
  it('should delete an existing user', async () => {
    await request(app)
      .delete('/api/users/1')
      .expect(204);

    // Confirmar que foi excluido
    await request(app)
      .get('/api/users/1')
      .expect(404);
  });

  it('should return 404 for non-existent user', async () => {
    await request(app)
      .delete('/api/users/999')
      .expect(404);
  });
});

Step 6: Padroes de teste e melhores praticas

Padrao AAA (Arrange-Act-Assert)

Estruture os testes em tres fases:

it('should create a new user', async () => {
  // Arrange: Preparar dados de teste
  const newUser = {
    name: '山田次郎',
    email: 'yamada@example.com'
  };

  // Act: Executar o que esta sendo testado
  const response = await request(app)
    .post('/api/users')
    .send(newUser);

  // Assert: Verificar os resultados
  expect(response.status).toBe(201);
  expect(response.body).toMatchObject(newUser);
});

Manter independencia dos testes

describe('Users API', () => {
  // Reset dos dados antes de cada teste
  beforeEach(() => {
    resetUsers();
  });

  // Limpeza apos cada teste (se necessario)
  afterEach(() => {
    // Reset de mocks, etc.
    jest.clearAllMocks();
  });

  // Limpeza apos todos os testes
  afterAll(async () => {
    // Fechar conexao com DB, etc.
  });
});

Nomes de teste descritivos

// Bom exemplo: Especifico e intencao clara
it('should return 404 when user does not exist', () => {});
it('should create user and return 201 status', () => {});
it('should validate email format and reject invalid emails', () => {});

// Mau exemplo: Intencao nao clara
it('test1', () => {});
it('works', () => {});
it('success', () => {});

Convencao de nomenclatura: O formato “should + comportamento esperado” e comum.

Testes de valores limite

describe('Validation', () => {
  it('should accept name with 1 character (minimum)', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({ name: 'A', email: 'test@example.com' })
      .expect(201);
  });

  it('should accept name with 100 characters (maximum)', async () => {
    const longName = 'A'.repeat(100);
    const response = await request(app)
      .post('/api/users')
      .send({ name: longName, email: 'test@example.com' })
      .expect(201);
  });

  it('should reject empty name', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({ name: '', email: 'test@example.com' })
      .expect(400);
  });
});

Step 7: Testes de API com autenticacao

Exemplo de testes de API com autenticacao JWT:

describe('Protected API', () => {
  const validToken = 'Bearer valid-jwt-token';
  const invalidToken = 'Bearer invalid-token';

  it('should return 401 without authorization header', async () => {
    await request(app)
      .get('/api/protected/resource')
      .expect(401);
  });

  it('should return 401 with invalid token', async () => {
    await request(app)
      .get('/api/protected/resource')
      .set('Authorization', invalidToken)
      .expect(401);
  });

  it('should return 200 with valid token', async () => {
    await request(app)
      .get('/api/protected/resource')
      .set('Authorization', validToken)
      .expect(200);
  });
});

Step 8: Uso de mocks

Criando mocks para dependencias externas (banco de dados, APIs externas).

Mock de banco de dados

// src/services/userService.ts
import { db } from '../db';

export const userService = {
  findAll: async () => db.users.findMany(),
  findById: async (id: number) => db.users.findUnique({ where: { id } }),
  create: async (data: { name: string; email: string }) => db.users.create({ data })
};
// src/services/userService.test.ts
import { userService } from './userService';
import { db } from '../db';

// Mock do modulo db
jest.mock('../db', () => ({
  db: {
    users: {
      findMany: jest.fn(),
      findUnique: jest.fn(),
      create: jest.fn()
    }
  }
}));

describe('UserService', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should return all users', async () => {
    const mockUsers = [
      { id: 1, name: 'User 1', email: 'user1@example.com' }
    ];

    (db.users.findMany as jest.Mock).mockResolvedValue(mockUsers);

    const result = await userService.findAll();

    expect(db.users.findMany).toHaveBeenCalledTimes(1);
    expect(result).toEqual(mockUsers);
  });
});

Mock de API externa

import axios from 'axios';

jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

describe('External API integration', () => {
  it('should fetch data from external API', async () => {
    const mockData = { data: { id: 1, title: 'Test' } };
    mockedAxios.get.mockResolvedValue(mockData);

    const result = await fetchExternalData();

    expect(mockedAxios.get).toHaveBeenCalledWith('https://api.example.com/data');
    expect(result).toEqual(mockData.data);
  });

  it('should handle API errors', async () => {
    mockedAxios.get.mockRejectedValue(new Error('Network Error'));

    await expect(fetchExternalData()).rejects.toThrow('Network Error');
  });
});

Cobertura de testes

Gerando relatorio de cobertura

npm test -- --coverage
---------------------|---------|----------|---------|---------|
File                 | % Stmts | % Branch | % Funcs | % Lines |
---------------------|---------|----------|---------|---------|
All files            |   95.24 |    88.89 |   100   |   95.24 |
 app.ts              |   95.24 |    88.89 |   100   |   95.24 |
---------------------|---------|----------|---------|---------|

Tipos de cobertura

TipoDescricao
StatementsTaxa de execucao de declaracoes
BranchesTaxa de cobertura de ramificacoes condicionais
FunctionsTaxa de chamadas de funcoes
LinesTaxa de execucao de linhas

Nota: 100% de cobertura nao e o objetivo. Priorize testar a logica de negocios importante.

Erros comuns e antipadroes

1. Dependencia entre testes

// Mau exemplo: Depende da ordem dos testes
it('should create user', async () => {
  await request(app).post('/api/users').send({ name: 'Test', email: 'test@example.com' });
});

it('should have 3 users', async () => {
  // Assume que o teste acima foi executado
  const response = await request(app).get('/api/users');
  expect(response.body.length).toBe(3);
});

// Bom exemplo: Cada teste e independente
beforeEach(() => {
  resetUsers(); // Reset dos dados
});

it('should create user', async () => {
  const response = await request(app)
    .post('/api/users')
    .send({ name: 'Test', email: 'test@example.com' });
  expect(response.status).toBe(201);
});

2. Testar detalhes de implementacao

// Mau exemplo: Testa implementacao interna
it('should call database with correct SQL', async () => {
  await userService.findById(1);
  expect(db.query).toHaveBeenCalledWith('SELECT * FROM users WHERE id = 1');
});

// Bom exemplo: Testa o comportamento
it('should return user by id', async () => {
  const user = await userService.findById(1);
  expect(user.id).toBe(1);
});

3. Excesso de mocks

// Mau exemplo: Mock de tudo (teste sem sentido)
jest.mock('./userService');
jest.mock('./database');
jest.mock('./validator');

// Bom exemplo: Mock apenas de dependencias externas
jest.mock('./externalApiClient');

Resumo

Ao escrever testes de API, voce obtem os seguintes beneficios:

  • Prevencao de regressao (quebra de funcionalidades existentes)
  • Documentacao da especificacao da API em codigo
  • Confianca ao refatorar
  • Verificacao automatica no pipeline CI/CD

Comece com testes simples de GET/POST e gradualmente expanda a cobertura.

Documentacao oficial

Melhores praticas e artigos

Livros

  • “Test Driven Development” (Kent Beck) - A obra original sobre TDD
  • “Refactoring” (Martin Fowler) - Relacao entre testes e refatoracao

Ferramentas

  • Jest - Framework de testes JavaScript
  • Vitest - Framework de testes rapido para Vite
  • Postman - Ferramenta de desenvolvimento e teste de API
  • Insomnia - Cliente REST/GraphQL
← Voltar para a lista