kb.erickguedes.com
Node.js: Backend JavaScript em Produção

Testes e Debugging

Aula 5 de 7

Testing com Jest e Vitest

npm install -D jest
npm install -D vitest
// vitest.config.js
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'lcov']
    }
  }
});

Testes Unitários

// src/math.js
function sum(a, b) { return a + b; }
function divide(a, b) {
  if (b === 0) throw new Error('Divisão por zero');
  return a / b;
}

module.exports = { sum, divide };
// tests/math.test.js
const { sum, divide } = require('../src/math');

describe('Math', () => {
  describe('sum', () => {
    it('deve somar dois números positivos', () => {
      expect(sum(2, 3)).toBe(5);
    });

    it('deve somar números negativos', () => {
      expect(sum(-1, -2)).toBe(-3);
    });
  });

  describe('divide', () => {
    it('deve dividir dois números', () => {
      expect(divide(10, 2)).toBe(5);
    });

    it('deve lançar erro ao dividir por zero', () => {
      expect(() => divide(5, 0)).toThrow('Divisão por zero');
    });
  });
});

Testes de API com Supertest

npm install -D supertest
// src/app.js
const express = require('express');
const app = express();

app.use(express.json());

app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

app.post('/users', (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: 'Nome e email são obrigatórios' });
  }
  res.status(201).json({ id: 1, name, email });
});

module.exports = app;
// tests/api.test.js
const request = require('supertest');
const app = require('../src/app');

describe('API', () => {
  it('GET /health deve retornar status ok', async () => {
    const res = await request(app).get('/health');
    expect(res.status).toBe(200);
    expect(res.body.status).toBe('ok');
  });

  it('POST /users deve criar usuário', async () => {
    const res = await request(app)
      .post('/users')
      .send({ name: 'Alice', email: '[email protected]' });

    expect(res.status).toBe(201);
    expect(res.body).toHaveProperty('id');
    expect(res.body.name).toBe('Alice');
  });

  it('POST /users deve validar campos obrigatórios', async () => {
    const res = await request(app)
      .post('/users')
      .send({ name: 'Alice' });

    expect(res.status).toBe(400);
    expect(res.body.error).toBeDefined();
  });
});

Mocking

// src/users.js
const prisma = require('./prisma');

async function getUsers() {
  return prisma.user.findMany({
    where: { active: true },
    select: { id: true, name: true, email: true }
  });
}

module.exports = { getUsers };
// tests/users.test.js
const { getUsers } = require('../src/users');
const prisma = require('../src/prisma');

jest.mock('../src/prisma', () => ({
  user: {
    findMany: jest.fn()
  }
}));

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

  it('deve retornar usuários ativos', async () => {
    const mockUsers = [
      { id: 1, name: 'Alice', email: '[email protected]' },
      { id: 2, name: 'Bob', email: '[email protected]' }
    ];

    prisma.user.findMany.mockResolvedValue(mockUsers);

    const result = await getUsers();

    expect(result).toEqual(mockUsers);
    expect(prisma.user.findMany).toHaveBeenCalledWith({
      where: { active: true },
      select: { id: true, name: true, email: true }
    });
  });

  it('deve retornar array vazio se não houver usuários', async () => {
    prisma.user.findMany.mockResolvedValue([]);

    const result = await getUsers();
    expect(result).toEqual([]);
  });
});

Sinon (stubs/spies)

const sinon = require('sinon');

describe('Com Sinon', () => {
  it('deve spy em uma função', () => {
    const callback = sinon.spy();
    callback('argumento');
    expect(callback.calledWith('argumento')).toBe(true);
  });

  it('deve stub uma função', () => {
    const stub = sinon.stub().returns(42);
    expect(stub()).toBe(42);
  });
});

Debugging

Node.js --inspect

# Modo debug
node --inspect src/server.js
node --inspect-brk src/server.js  # pausa na primeira linha

# Debug com Chrome DevTools
# Abra chrome://inspect no Chrome

ndb (alternative)

npm install -g ndb
ndb src/server.js

VS Code (launch.json)

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Server",
      "skipFiles": ["<node_internals>/**"],
      "program": "${workspaceFolder}/src/server.js"
    },
    {
      "type": "node",
      "request": "attach",
      "name": "Attach to Process",
      "port": 9229
    }
  ]
}

Logging Estruturado

Pino (rápido)

const pino = require('pino');

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport: {
    target: 'pino-pretty',
    options: { colorize: true }
  }
});

logger.info({ userId: 123 }, 'Usuário logado');
logger.error({ err: new Error('Falha na conexão') }, 'Erro no banco');
logger.debug('Mensagem de debug');

// Contexto child
const requestLogger = logger.child({ requestId: 'abc-123' });
requestLogger.info('Processando requisição');

Winston

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
    new winston.transports.Console({
      format: winston.format.simple()
    })
  ]
});

logger.info({ message: 'Servidor iniciado', port: 3000 });

Lab: Exercício - Suite de Testes Completa

// tests/integration/users.test.js
const request = require('supertest');
const app = require('../../src/app');
const prisma = require('../../src/prisma');

// Setup e teardown
beforeAll(async () => {
  await prisma.$connect();
});

afterAll(async () => {
  await prisma.$disconnect();
});

beforeEach(async () => {
  await prisma.user.deleteMany();
});

describe('Users API', () => {
  it('deve listar usuários vazia', async () => {
    const res = await request(app).get('/users');
    expect(res.status).toBe(200);
    expect(res.body).toEqual([]);
  });

  it('deve criar e listar usuários', async () => {
    await request(app)
      .post('/users')
      .send({ name: 'Alice', email: '[email protected]', password: '123' });

    const res = await request(app).get('/users');
    expect(res.status).toBe(200);
    expect(res.body).toHaveLength(1);
    expect(res.body[0].name).toBe('Alice');
  });

  it('deve retornar 404 para usuário inexistente', async () => {
    const res = await request(app).get('/users/999');
    expect(res.status).toBe(404);
  });
});
# Rodar testes
npm test
npm run test:coverage
npm run test:watch

# Modo debug
node --inspect src/server.js

Use Vitest para projetos modernos (mais rápido, compatível com Jest API). Sempre faça mock de dependências externas (banco, API). Supertest permite testar endpoints Express sem precisar subir o servidor. Pino é 5x mais rápido que Winston em produção.