▶ Slides ← MĂłdulo
Módulo 2 · Ciclo Comum · IN02

Back-End I — TDD top-down

Aula 5 — Material de Leitura Aprofundado

1. Por que TDD top-down?

AtĂ© a aula 4 vocĂȘ escreveu SQL puro: queries, JOINs, agregaçÔes executadas dentro do cliente do banco. Isso resolve parte de um sistema — mas nĂŁo hĂĄ aplicação ainda. A partir desta aula, esse SQL passa a ser invocado por cĂłdigo de aplicação: um servidor HTTP que recebe requisiçÔes, chama o banco, formata a resposta e retorna ao cliente. É o back-end.

Em vez de escrever todo o servidor de uma vez e depois tentar testar, vamos fazer o caminho inverso: escrever o teste primeiro, ver ele falhar, escrever o cĂłdigo mĂ­nimo que faz passar, repetir. Esse Ă© o ciclo RED → GREEN → REFACTOR do TDD. E faremos isso top-down: começamos pelo controller (o ponto de entrada HTTP), descemos para o service, depois o repository, e finalmente o helper puro.

🔮
RED
Escreva o teste. Ele falha porque o código ainda não existe — e está certo que falhe.
🟱
GREEN
Escreva o mĂ­nimo de cĂłdigo que faz o teste passar. Sem inventar funcionalidade extra.
♻
REFACTOR
Com o teste verde, refatore com segurança: extraia funçÔes, renomeie, reorganize.

Por que TypeScript no back-end?

Existem trĂȘs grandes razĂ”es para o back-end deste curso ser em TypeScript em vez de JavaScript puro: tipagem estĂĄtica que pega erros antes de rodar; contratos explĂ­citos entre camadas — uma interface User Ă© a verdade compartilhada por controller, service e repository, e o compilador reclama imediatamente se uma camada quebrar o contrato; e refactor seguro — renomear um campo de modelo quebra a compilação em todos os lugares onde ele Ă© usado de forma inconsistente. No fim, TypeScript nĂŁo Ă© "JavaScript com chatice" — Ă© JavaScript com um par programador que checa cada linha que vocĂȘ escreve.

Conceito fundamental

TDD top-down responde uma pergunta de cada vez. Spec do controller pergunta "como o sistema Ă© usado?". Spec do service pergunta "qual Ă© a regra de negĂłcio?". Spec do repository pergunta "como persistimos?". Spec do helper pergunta "essa lĂłgica isolada estĂĄ certa?". Cada nĂ­vel protege o seguinte.

O alvo de hoje

Ao fim do dia, sua aplicação responderĂĄ GET /users/:id retornando o usuĂĄrio em JSON quando ele existir, e 404 quando nĂŁo. Isso parece simples — Ă© simples. Mas Ă© o caminho completo: HTTP entra, controller delega ao service, service consulta o repository, repository pergunta ao Postgres via pg.Pool, e a resposta sobe de volta. Cada elo da corrente terĂĄ seu spec.

2. Setup do projeto TypeScript

O setup Ă© declarativo: trĂȘs comandos no terminal, dois arquivos de configuração, e cinco scripts no package.json. Nada de mĂĄgica.

terminal — bootstrap do projeto
# 1. Inicializa o package.json
npm init -y

# 2. DependĂȘncias de runtime
npm install express pg dotenv

# 3. DependĂȘncias de desenvolvimento
npm install -D typescript @types/node @types/express @types/pg \
  tsx jest ts-jest @types/jest supertest @types/supertest

# 4. Inicializa o tsconfig.json
npx tsc --init

Depois de rodar esses comandos vocĂȘ terĂĄ um package.json, um tsconfig.json com configuração padrĂŁo e um node_modules/. Edite o tsconfig.json para deixar a configuração estrita — isso evita uma classe inteira de bugs.

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "rootDir": "src",
    "outDir": "dist",
    "resolveJsonModule": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules", "dist"]
}

E os scripts do package.json que vocĂȘ usa todo dia:

package.json — campo "scripts"
"scripts": {
  "dev":        "tsx watch src/server.ts",
  "build":      "tsc",
  "start":      "node dist/server.js",
  "test":       "jest",
  "test:watch": "jest --watch"
}
tsx vs ts-node

Usamos tsx para o modo dev porque Ă© mais rĂĄpido (usa esbuild) e tem watch nativo. ts-node ainda Ă© popular, mas tsx Ă© a escolha moderna para projetos Express simples.

3. Setup Jest + ts-jest + supertest

O Jest Ă© o framework de testes mais popular do ecossistema Node. Ele descobre arquivos com sufixo .test.ts ou .spec.ts, executa cada um isoladamente e relata pass/fail. Vamos adotar a convenção .spec.ts — "specification file" — e colocar cada spec ao lado do arquivo que ele testa. O ts-jest ensina o Jest a entender TypeScript em memĂłria, sem build prĂ©vio.

O supertest Ă© uma peça-chave para testar controllers: ele dispara requisiçÔes HTTP contra o objeto app do Express sem subir uma porta de rede. O servidor Ă© instanciado em memĂłria, a requisição vai e volta, e vocĂȘ inspeciona res.status, res.body. É rĂĄpido, isolado e perfeito para CI.

jest.config.ts
import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  testMatch: ['**/*.spec.ts'],
  collectCoverageFrom: ['src/**/*.ts', '!src/**/*.spec.ts'],
  clearMocks: true
};

export default config;

Cada chave acima resolve um problema especĂ­fico. preset: 'ts-jest' ativa a transformação TS-em-JS em memĂłria. testEnvironment: 'node' evita carregar o JSDOM (que sĂł faz sentido para testes de front-end). testMatch diz que todo arquivo terminado em .spec.ts Ă© um teste — isso permite o padrĂŁo "spec ao lado do arquivo". collectCoverageFrom mira sĂł na pasta src/, ignorando os prĂłprios specs. E clearMocks: true limpa todos os mocks entre testes — sem ele, o estado de um teste vaza para o prĂłximo.

Atenção — banco real nos specs?

Nesta aula, todos os specs sĂŁo unitĂĄrios — eles mockam o pool ou o repository. NĂŁo tocamos no banco real. Em aulas futuras, faremos testes de integração com um banco efĂȘmero (Postgres em container) que sobe e cai a cada suĂ­te. Para hoje, mocks bastam.

4. AAA + matchers

Todo teste bem escrito tem trĂȘs blocos visualmente separados: Arrange, Act, Assert. Esse padrĂŁo Ă© tĂŁo Ăștil que ganhou nome — AAA. Ele transforma o spec em uma especificação legĂ­vel: a primeira parte prepara o cenĂĄrio, a segunda dispara a ação, a terceira verifica o resultado.

AAA — exemplo genĂ©rico
it('soma dois nĂșmeros', () => {
  // ── Arrange ──────────────────────
  const a = 2;
  const b = 3;

  // ── Act ──────────────────────────
  const resultado = somar(a, b);

  // ── Assert ───────────────────────
  expect(resultado).toBe(5);
});

Os matchers do Jest sĂŁo as funçÔes que vocĂȘ encadeia depois de expect() para fazer a verificação. Conhecer um pequeno conjunto cobre 90% dos casos:

MatcherVerificaQuando usar
toBeIgualdade estrita (===)Primitivos: number, string, boolean
toEqualIgualdade profundaObjetos e arrays — compara conteĂșdo, nĂŁo referĂȘncia
toMatchObjectSubset de propriedadesQuando vocĂȘ sĂł quer verificar algumas chaves de um objeto
toHaveBeenCalledWithMock chamado com XVerificar argumentos passados a uma função mockada
rejects.toThrowPromise rejeitada com erroCasos de erro em cĂłdigo async
toBeInstanceOfÉ instñncia de uma classePara erros customizados (NotFoundError etc.)
Erro comum

Usar toBe({ id: 1 }) em um objeto. Falha sempre — toBe compara referĂȘncia. O correto para objetos Ă© toEqual (profundo) ou toMatchObject (subset).

5. REDSpec do Controller via supertest

Começamos pelo topo. O controller Ă© a casca do back-end: o ponto onde uma requisição HTTP entra. Antes de existir uma linha do controller, vamos descrever em cĂłdigo como ele deve se comportar. O supertest torna isso natural: vocĂȘ importa o app do Express, dispara uma requisição e inspeciona a resposta.

src/controllers/userController.spec.ts
import request from 'supertest';
import { app } from '../app';
import * as svc from '../services/userService';

jest.mock('../services/userService');

describe('GET /users/:id', () => {
  it('200 com user existente', async () => {
    // Arrange
    (svc.getById as jest.Mock).mockResolvedValue({
      id: 1, name: 'Ana', email: 'ana@inteli.edu'
    });

    // Act
    const res = await request(app).get('/users/1');

    // Assert
    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({ id: 1, name: 'Ana' });
  });

  it('404 quando user nĂŁo existe', async () => {
    (svc.getById as jest.Mock).mockRejectedValue(
      new NotFoundError('User 999 not found')
    );

    const res = await request(app).get('/users/999');

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

Note trĂȘs decisĂ”es importantes. Primeiro, jest.mock('../services/userService') substitui o mĂłdulo inteiro por mocks automĂĄticos — quando o controller importar svc.getById, ele receberĂĄ uma função fake, nĂŁo a real. Segundo, controlamos o que essa função fake retorna com mockResolvedValue (resolve a Promise com valor) e mockRejectedValue (rejeita com erro). Terceiro, o teste nĂŁo toca no banco — o service Ă© fake, entĂŁo nem o repository nem o pool sĂŁo chamados.

Resultado esperado: VERMELHO

Esse teste falha ao rodar. NĂŁo existe app, nĂŁo existe rota, nĂŁo existe controller. A falha Ă© o ponto de partida — ela define exatamente o que precisamos construir.

6. REDSpec do Service mockando o Repository

O service guarda a regra de negĂłcio. Aqui ela Ă© simples: "se o repository devolveu null, lance NotFoundError; senĂŁo, devolva o user". Em sistemas reais, services contĂȘm validaçÔes de domĂ­nio, orquestração de mĂșltiplos repositories, regras de transação. Hoje, a regra Ă© mĂ­nima — mas o spec descreve exatamente os dois caminhos.

src/services/userService.spec.ts
import * as repo from '../repositories/userRepository';
import { getById } from './userService';
import { NotFoundError } from '../errors/AppError';

jest.mock('../repositories/userRepository');

describe('userService.getById', () => {
  it('retorna o user quando encontrado', async () => {
    (repo.findById as jest.Mock).mockResolvedValue({
      id: 1, name: 'Ana', email: 'ana@inteli.edu'
    });

    const user = await getById(1);

    expect(user).toMatchObject({ id: 1, name: 'Ana' });
    expect(repo.findById).toHaveBeenCalledWith(1);
  });

  it('lança NotFoundError quando o repository retorna null', async () => {
    (repo.findById as jest.Mock).mockResolvedValue(null);

    await expect(getById(999)).rejects.toThrow(NotFoundError);
  });
});

Os dois testes cobrem os dois caminhos do service. O primeiro Ă© o caminho feliz: repo entrega user, service repassa. O segundo Ă© o caminho infeliz: repo entrega null, service traduz para erro de domĂ­nio. Note como o repository nĂŁo existe ainda — estamos descrevendo a interface esperada (findById(id) retorna User | null) atravĂ©s do mock.

Test doubles — vocabulário

Stub: dummy que retorna valor fixo. Mock: stub que tambĂ©m grava como foi chamado (para vocĂȘ verificar argumentos). Spy: envolve a função real, observando sem trocar. O Jest oferece tudo via jest.fn(), jest.mock() e jest.spyOn().

7. REDSpec do Repository — SQL parametrizado

O repository Ă© a Ășnica camada que sabe falar SQL. O spec dele tem dois objetivos: provar que a query usa marcadores parametrizados ($1, nĂŁo concatenação) e que o pool Ă© chamado com o valor certo. Mockamos o mĂłdulo db/pool para nĂŁo tocar em banco real.

src/repositories/userRepository.spec.ts
import { pool } from '../db/pool';
import { findById } from './userRepository';

jest.mock('../db/pool', () => ({
  pool: { query: jest.fn() }
}));

describe('userRepository.findById', () => {
  it('usa SQL parametrizado com $1', async () => {
    (pool.query as jest.Mock).mockResolvedValue({
      rows: [{ id: 1, name: 'Ana' }]
    });

    await findById(1);

    expect(pool.query).toHaveBeenCalledWith(
      'SELECT * FROM users WHERE id = $1',
      [1]
    );
  });

  it('retorna null quando nĂŁo hĂĄ linhas', async () => {
    (pool.query as jest.Mock).mockResolvedValue({ rows: [] });

    const r = await findById(999);

    expect(r).toBeNull();
  });
});

O matcher toHaveBeenCalledWith faz a verificação principal: ele afirma que pool.query foi chamado com exatamente esses dois argumentos — a string SQL com $1 e o array [1]. Isso Ă© mais forte do que sĂł verificar que a query roda; Ă© uma proteção contra SQL injection: se alguĂ©m amanhĂŁ trocar para "WHERE id = " + id, esse teste falha.

Banco efĂȘmero — quando precisa

Mockar o pool Ă© suficiente para provar a estrutura da query. Mas para garantir que o SQL Ă© sintaticamente vĂĄlido e que o resultado Ă© mapeado corretamente, em algum momento vocĂȘ precisarĂĄ de um banco real. A solução padrĂŁo Ă© o banco efĂȘmero: um container Docker do Postgres que sobe a cada suĂ­te de testes de integração e Ă© descartado depois. Veremos isso em aulas futuras.

8. REDSpec do Helper puro — isValidEmail

Helpers sĂŁo funçÔes puras: dado o mesmo input, devolvem o mesmo output, sem efeitos colaterais. SĂŁo o caso mais simples de testar — nĂŁo precisa de mock, nĂŁo precisa de async, nĂŁo precisa de banco. Sirvem como aquecimento e como rede de segurança para validaçÔes que migram entre camadas.

src/helpers/email.spec.ts
import { isValidEmail } from './email';

describe('isValidEmail', () => {
  it('aceita email vĂĄlido', () => {
    expect(isValidEmail('ana@inteli.edu')).toBe(true);
    expect(isValidEmail('bruno.costa+vip@empresa.com.br')).toBe(true);
  });

  it('rejeita string sem @', () => {
    expect(isValidEmail('foo')).toBe(false);
  });

  it('rejeita string sem domĂ­nio', () => {
    expect(isValidEmail('ana@')).toBe(false);
  });

  it('rejeita string vazia', () => {
    expect(isValidEmail('')).toBe(false);
  });
});

Cobrir os casos felizes e os infelizes em testes diferentes deixa a falha falando: se a regex aceita uma string sem domĂ­nio, sabemos exatamente qual it quebrou e qual ajuste Ă© preciso. Esse Ă© o ponto do TDD — o teste Ă© a documentação e o detector de regressĂŁo ao mesmo tempo.

9. Estrutura de pastas — .spec.ts ao lado

O MVC clĂĄssico tem trĂȘs pastas. Em projetos Node modernos, vocĂȘ verĂĄ uma evolução para seis camadas, cada uma com responsabilidade clara. Essa Ă© a organização que adotamos no curso a partir de agora.

đŸ›Łïž routes
Mapeia URL para controller. Sem lĂłgica.
🎼 controllers
Recebe req, valida formato, delega ao service, devolve res. Sem regra de negĂłcio.
🧠 services
Regras de negócio. Orquestra repositories. Lança erros de domínio.
đŸ—„ïž repositories
SQL e pool. Única camada que conhece o banco.
📐 models
Interfaces TS — contratos compartilhados.
đŸ› ïž helpers
FunçÔes puras reutilizåveis.

E os specs ficam ao lado do arquivo testado. NĂŁo em uma pasta __tests__/ separada. Por quĂȘ? Porque quando vocĂȘ renomeia userService.ts, o userService.spec.ts aparece junto no diff. Quando vocĂȘ muda de pasta, ambos vĂŁo. Ficam fisicamente acoplados — o que Ă© Ăłtimo, porque conceitualmente eles sĂŁo uma coisa sĂł.

ĂĄrvore de arquivos
src/
├── controllers/
│   ├── userController.ts
│   └── userController.spec.ts        // supertest
├── routes/
│   └── userRoutes.ts
├── services/
│   ├── userService.ts
│   └── userService.spec.ts           // mock do repo
├── repositories/
│   ├── userRepository.ts
│   └── userRepository.spec.ts        // mock do pool
├── models/
│   └── user.ts                       // interface User
├── helpers/
│   ├── email.ts
│   └── email.spec.ts                 // puro
├── middlewares/
│   └── errorHandler.ts
├── errors/
│   └── AppError.ts
├── db/
│   └── pool.ts
├── app.ts
└── server.ts

10. GREENController e Rotas — primeiro

Hora de pintar verde. Começamos pelo controller e pela rota — nessa ordem — porque o spec do controller Ă© o que falha mais alto na pilha. Implementamos o mĂ­nimo: receber a requisição, chamar o service, devolver o JSON. Sem validaçÔes elaboradas, sem cache, sem nada que o teste nĂŁo exija.

src/controllers/userController.ts
import type { Request, Response, NextFunction } from 'express';
import * as svc from '../services/userService';

export async function show(
  req: Request,
  res: Response,
  next: NextFunction
) {
  try {
    const id = Number(req.params.id);
    const user = await svc.getById(id);
    res.json(user);
  } catch (e) {
    next(e);
  }
}

O controller chama svc.getById, captura qualquer erro e o repassa via next(e) — o middleware de erro (que veremos adiante) traduz NotFoundError para HTTP 404.

src/routes/userRoutes.ts
import { Router } from 'express';
import { show } from '../controllers/userController';

export const userRoutes = Router();

userRoutes.get('/:id', show);

E o app.ts amarra tudo:

src/app.ts
import express from 'express';
import { userRoutes } from './routes/userRoutes';
import { errorHandler } from './middlewares/errorHandler';

export const app = express();

app.use(express.json());
app.use('/users', userRoutes);
app.use(errorHandler);

E o server.ts, finalmente, sobe a porta — mas não importa para os testes, porque o supertest usa o objeto app diretamente:

src/server.ts
import { app } from './app';

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`API rodando em http://localhost:${port}`);
});
Por que separar app e server?

Mantemos app.ts sem listen(). Isso permite que o supertest importe o app e dispare requisiçÔes sem subir uma porta real. O server.ts Ă© o Ășnico lugar que dĂĄ listen — e ele sĂł roda em produção/dev, nĂŁo em teste.

11. GREENService — a regra de negócio

O service descobre se o user existe (perguntando ao repository) e decide o que fazer com a resposta. Hoje a regra Ă© uma sĂł: se nĂŁo existe, lance NotFoundError. Em sistemas maiores, esse mesmo mĂ©todo poderia tambĂ©m checar permissĂŁo, calcular dados derivados, gravar log de auditoria — toda regra de domĂ­nio mora aqui.

src/services/userService.ts
import { findById } from '../repositories/userRepository';
import type { User } from '../models/user';
import { NotFoundError } from '../errors/AppError';

export async function getById(id: number): Promise<User> {
  const user = await findById(id);
  if (!user) {
    throw new NotFoundError(`User ${id} not found`);
  }
  return user;
}

E o model — uma Ășnica interface compartilhada por todas as camadas:

src/models/user.ts
export interface User {
  id: number;
  name: string;
  email: string;
  created_at?: Date;
}
Models como contratos

A interface User Ă© importada por todas as camadas que tocam o conceito "usuĂĄrio". Se amanhĂŁ vocĂȘ adicionar um campo role obrigatĂłrio, o TypeScript reclama em todos os lugares que ainda nĂŁo enviam ou nĂŁo consomem esse campo. É o contrato vivo entre as camadas.

12. GREENRepository + Pool — SQL parametrizado

O repository Ă© a fronteira entre a aplicação e o banco. Toda consulta SQL deve viver aqui — nunca em controllers ou services. Isso facilita revisĂŁo de segurança (vocĂȘ sabe onde procurar), facilita troca de banco (se um dia migrar para outro driver, sĂł essa camada muda) e facilita teste (vocĂȘ pode mockar a função inteira em vez de mockar o pool).

src/db/pool.ts
import { Pool } from 'pg';
import dotenv from 'dotenv';

dotenv.config();

export const pool = new Pool({
  connectionString: process.env.DATABASE_URL
});

O pg.Pool mantém um conjunto de conexÔes reutilizåveis. Isso é crítico em produção: abrir uma conexão TCP por requisição é caríssimo. O pool reutiliza, balanceia e descarta automaticamente.

src/repositories/userRepository.ts
import { pool } from '../db/pool';
import type { User } from '../models/user';

export async function findById(id: number): Promise<User | null> {
  const r = await pool.query<User>(
    'SELECT * FROM users WHERE id = $1',
    [id]
  );
  return r.rows[0] ?? null;
}

Repare em trĂȘs detalhes que parecem pequenos mas sĂŁo fundamentais. O marcador $1 com array [id] separa cĂłdigo SQL de dado do usuĂĄrio — o driver do pg escapa o valor automaticamente, eliminando SQL injection. O tipo genĂ©rico <User> diz ao TypeScript que r.rows Ă© User[], dando autocomplete e checagem do tipo de retorno. O operador ?? (nullish coalescing) traduz "linha inexistente" em null explĂ­cito — evita devolver undefined, que Ă© uma sutileza que pode confundir o service.

SQL injection — não negociável

Nunca: "SELECT * FROM users WHERE id = " + req.params.id. O usuĂĄrio pode mandar 1; DROP TABLE users;-- e isso Ă© executado. Sempre use marcadores $1, $2, ... com o array de valores. O driver do pg faz o escape correto para vocĂȘ.

13. GREENHelpers + Errors + Middleware

Faltam trĂȘs peças: o erro de domĂ­nio, o middleware que traduz para HTTP, e o helper puro. Os trĂȘs juntos fecham o caminho do erro do banco de volta para o cliente.

src/errors/AppError.ts
export class AppError extends Error {
  constructor(
    msg: string,
    public status = 500
  ) {
    super(msg);
  }
}

export class NotFoundError extends AppError {
  constructor(msg = 'Not Found') {
    super(msg, 404);
  }
}

Erros de domínio são classes, não strings. Isso permite que o middleware decida o que fazer baseado no instanceof, e då ao service vocabulårio para expressar a intenção: "esse user não foi encontrado" é diferente de "o banco caiu". Cada um vira uma classe.

src/middlewares/errorHandler.ts
import type { ErrorRequestHandler } from 'express';
import { AppError } from '../errors/AppError';

export const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
  if (err instanceof AppError) {
    return res.status(err.status).json({ error: err.message });
  }
  console.error(err);
  res.status(500).json({ error: 'Internal Server Error' });
};

O middleware Ă© um interceptador de erro: o Express chama-o quando algum middleware ou handler chama next(err). Se for um AppError nosso, devolvemos o status correto (404, 400, 422, etc.) com mensagem amigĂĄvel. Se for qualquer outro erro, devolvemos 500 com mensagem genĂ©rica — e logamos o erro real no servidor (nunca exponha stack trace ao cliente em produção).

src/helpers/email.ts
const RX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export function isValidEmail(s: string): boolean {
  return RX.test(s);
}

O helper Ă© proposital simples — Ă© um detector bĂĄsico, nĂŁo validador definitivo de RFC 5322 (que Ă© absurdamente complexo). Para a maioria dos casos prĂĄticos, "tem caractere antes do @, depois do @ e tem ponto no domĂ­nio" Ă© o suficiente. Se vocĂȘ precisa de mais rigor, use uma biblioteca como validator; mas comece simples.

Tudo verde — agora podemos refatorar

Com os 4 specs passando, temos a rede de segurança completa. npm test vira o sinal verde para mexer no código com confiança.

14. REFACTORRefatorar com testes verdes

Refatoração Ă© mudar a forma do cĂłdigo sem mudar o comportamento. É exatamente onde TDD brilha: vocĂȘ sabe que o comportamento nĂŁo mudou porque os testes continuam verdes. Sem testes, refatorar Ă© apostar; com testes, Ă© trabalho de oficina.

Extrair asyncHandler

Toda função controller que usamos repete o padrĂŁo try { ... } catch (e) { next(e); }. Isso Ă© ruĂ­do. Podemos extrair um wrapper que faz isso uma Ășnica vez:

src/helpers/asyncHandler.ts
import type { RequestHandler } from 'express';

export const asyncHandler =
  (fn: RequestHandler): RequestHandler =>
  (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };

Com isso, o controller fica:

src/controllers/userController.ts (refatorado)
import { asyncHandler } from '../helpers/asyncHandler';
import * as svc from '../services/userService';

export const show = asyncHandler(async (req, res) => {
  const user = await svc.getById(Number(req.params.id));
  res.json(user);
});

Rode npm test. Tudo verde? Boa, a refatoração foi correta. Vermelho? Algo quebrou — desfaça e tente outra abordagem. Esse Ă© o ritmo do TDD: passos pequenos, ciclos curtos.

Outras refatoraçÔes comuns

  • Extrair validação para helpers/ — o que era um if no controller vira uma função pura testĂĄvel.
  • Tipar callbacks com RequestHandler em vez de (req, res, next) soltos — mais conciso e seguro.
  • Promover constantes mĂĄgicas (status 404, mensagens) a constantes nomeadas — facilita mudança e leitura.
Aprofundamento

Existe uma diferença entre refactor e add feature. Refatorar nĂŁo muda comportamento — adicionar feature muda. Misturar os dois numa Ășnica mudança Ă© a fonte de muitos bugs. A regra de Kent Beck Ă©: troque de chapĂ©u. Quando estiver de "chapĂ©u de refactor", os testes nĂŁo mudam. Quando estiver de "chapĂ©u de feature", vocĂȘ primeiro escreve um novo teste vermelho.

15. Coverage — mĂ©trica, nĂŁo meta

Coverage (cobertura de cĂłdigo) Ă© a porcentagem do cĂłdigo fonte que Ă© executado durante os testes. O Jest mostra para vocĂȘ com jest --coverage:

terminal — saída de coverage
$ npm test -- --coverage

----------------------|---------|----------|---------|---------|
File                  | % Stmts | % Branch | % Funcs | % Lines |
----------------------|---------|----------|---------|---------|
All files             |   89.47 |    66.67 |   85.71 |   89.47 |
 controllers          |   100.00 |   100.00 |   100.00 |   100.00 |
 services             |   100.00 |   100.00 |   100.00 |   100.00 |
 repositories         |   100.00 |    50.00 |   100.00 |   100.00 |
 helpers              |   100.00 |   100.00 |   100.00 |   100.00 |
----------------------|---------|----------|---------|---------|

Coverage Ă© Ăștil para encontrar buracos — cĂłdigo que nunca Ă© executado nos testes. Mas Ă© pĂ©ssimo como meta. Buscar 100% de cobertura por si sĂł leva a testes vazios: testes sem asserção real, testes que apenas chamam funçÔes para "marcar a linha como coberta", testes que validam mocks em vez de comportamento. Esses testes mentem para vocĂȘ — o relatĂłrio fica verde mas o cĂłdigo estĂĄ sem rede.

HeurĂ­stica de qualidade

Se um teste pode ser deletado sem que vocĂȘ se sinta menos seguro, ele provavelmente estĂĄ mentindo. Foque em cobrir comportamentos, nĂŁo linhas. 80% de cobertura com testes de qualidade vale infinitamente mais do que 100% com testes vazios.

16. RM-ODP — onde mora o que escrevemos hoje

O framework RM-ODP (ISO/IEC 10746) define cinco viewpoints para descrever um sistema distribuído. O back-end de hoje toca todas elas, mas o foco principal é a visão Computational: como o sistema se decompÔe em componentes (controller, service, repository) com interfaces explícitas.

🏢
Enterprise (Empresarial)
"O sistema deve permitir consultar usuĂĄrios cadastrados via API HTTP" — esse Ă© o requisito de negĂłcio que justifica a existĂȘncia do endpoint GET /users/:id.
RF — propósito do recurso
📋
Information (Informação)
A interface User é a definição da informação. Ela é compartilhada por todas as camadas e estabelece o contrato sobre o que é um usuårio no sistema.
interface User = contrato compartilhado
Computational (Computacional) ◀ foco da aula
A decomposição em controller, service, repository, helpers. Cada componente tem uma interface clara (assinatura tipada). Os specs descrevem o contrato de cada componente.
controller / service / repository / helper
🔧
Engineering (Engenharia)
O pg.Pool que gerencia conexÔes TCP, o middleware que intercepta erros, o servidor Express que serializa JSON. Mecanismos que sustentam o sistema computacional.
Pool, middleware, JSON serialization
💻
Technology (Tecnologia)
Node.js LTS, TypeScript ES2022, Express 4, driver pg, Jest 29, supertest. A escolha tecnolĂłgica concreta.
Node + TypeScript + Express + pg

A escolha de TDD top-down nĂŁo Ă© gratuita — ela espelha a hierarquia das visĂ”es: começamos pelo uso (Computational, perto do Enterprise) e descemos atĂ© o mecanismo (Engineering/Technology). Cada spec Ă© um contrato em uma das visĂ”es.

17. RF, RN e RNF aplicados ao back-end

Requisitos Funcionais (RF)

Segundo Sommerville (2018), um RF descreve o que o sistema faz, e não como ele faz. Por isso, os enunciados abaixo evitam nomes de endpoint, classes ou helpers — esses detalhes ficam nas RNs e RNFs vinculadas. Cada endpoint do back-end implementa um ou mais RFs:

RFDescriçãoCamada principal
RF-04 O sistema deve permitir que o usuårio autenticado visualize as informaçÔes do seu próprio perfil. controller, service, repository
RF-05 O sistema deve retornar uma mensagem de "Recurso não encontrado" e interromper o processamento da solicitação caso o identificador do usuårio pesquisado seja inexistente ou invålido. service + middleware de erros
RF-06 O sistema deve impedir o cadastro de usuårios caso o e-mail fornecido não possua uma estrutura composta por um identificador, o símbolo "@" e um domínio vålido (por exemplo: nome@dominio.com). helper de validação + service

Regras de NegĂłcio (RN)

As RNs detalham as condiçÔes, restriçÔes e invariantes do domínio que cada RF deve respeitar. Vivem na camada de service (ou em filtros de repository) e podem ser verificadas isoladamente por testes unitårios.

RNTítuloDescriçãoVincula
RN-01 Identificação Ășnica Todo usuĂĄrio deve ser obrigatoriamente associado a um identificador imutĂĄvel, gerado automaticamente na criação do registro. RF-04
RN-02 Escopo do perfil A visualização do perfil deve retornar o conjunto completo de dados do usuårio, conforme definido no esquema de dados da conta (por exemplo, nome, e-mail e data de cadastro). RF-04
RN-03 Restrição de propriedade Um usuårio só pode acessar os detalhes do seu próprio perfil, impedindo o acesso a dados de terceiros por meio de manipulação de ID. RF-04
RN-04 Interrupção de fluxo Caso o identificador não seja localizado, nenhuma outra operação lógica (como registros de acesso ou cålculos de perfil) deve ser executada após a detecção do erro. RF-05

Requisitos NĂŁo Funcionais (RNF)

Os RNFs especificam como o sistema deve se comportar (qualidade, restriçÔes e padrÔes), seguindo os 8 eixos da ISO/IEC 25010 adotados na disciplina (USAB, CONF, DES, SUP, SEG, CAP, REST, ORG).

RNFEixoDescriçãoVincula
RNF-01 REST — Arquitetura de persistĂȘncia A recuperação de dados deve ser realizada por meio do padrĂŁo Repository, garantindo que a lĂłgica de acesso ao banco de dados esteja isolada da lĂłgica de negĂłcio (Service). RF-04
RNF-02 SUP — Protocolo A funcionalidade deve ser realizada por meio de um endpoint HTTP GET. RF-04
RNF-03 CONF — Padronização de erros O sistema deve utilizar um middleware global de exceçÔes para capturar erros do tipo NotFoundError e convertĂȘ-los automaticamente no cĂłdigo de status HTTP 404. RF-05
RNF-04 SEG — Segurança de resposta A mensagem de erro retornada não deve expor detalhes internos da infraestrutura ou do banco de dados (stack traces). RF-05
RNF-05 CONF — Sintaxe de contato A validação do e-mail deve seguir estritamente o padrão definido na RFC 5322, bloqueando caracteres especiais não permitidos no local-part. RF-06
RNF-06 SUP — Camada de validação A validação deve ocorrer na camada de serviço (business logic) antes de qualquer tentativa de persistĂȘncia, utilizando mĂ©todos utilitĂĄrios (helpers) especializados e testĂĄveis isoladamente. RF-06
RNF-07 REST — Resposta de validação Em caso de falha sintática, o sistema deve responder com o código de status HTTP 400 (Bad Request). RF-06
Como TDD ajuda RNFs

RNFs como manutenibilidade e segurança sĂŁo verificĂĄveis por testes. O spec do repository nĂŁo sĂł prova que a query funciona — ele impĂ”e o uso de marcadores parametrizados. Mudou a query para concatenação? O teste falha. Esse Ă© o poder do TDD: virar polĂ­ticas em cĂłdigo executĂĄvel.

18. Checklist de Estudo

Clique nos itens Ă  medida que dominar cada conceito:

  • Sei explicar por que TDD top-down começa pelo controller e nĂŁo pelo helper.
  • Configurei tsconfig.json em modo strict e entendo cada flag.
  • Configurei jest.config.ts com ts-jest e testMatch: ['**/*.spec.ts'].
  • Sei usar supertest para disparar requisiçÔes HTTP contra o app sem subir porta.
  • Escrevi um spec de controller que mocka o service e cobre 200 e 404.
  • Escrevi um spec de service que mocka o repository e cobre caminho feliz e NotFoundError.
  • Escrevi um spec de repository que mocka o pool e prova uso de $1.
  • Escrevi um spec de helper puro com vĂĄrios casos felizes e infelizes.
  • Implementei controller e rotas antes das outras camadas (top-down).
  • Sei diferenciar toBe, toEqual, toMatchObject e quando usar cada um.
  • Sei explicar AAA (Arrange / Act / Assert) com um exemplo prĂłprio.
  • Refatorei pelo menos uma duplicação com testes verdes (ex.: asyncHandler).
  • Sei explicar por que coverage Ă© mĂ©trica e nĂŁo meta.
  • Localizo o trabalho desta aula na visĂŁo Computational do RM-ODP.

ReferĂȘncias

Inteli Logo