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

Back-End I — TDD top-down

Aula 5 — Material de Leitura Aprofundado

Sobre este encontro

Back-End I · Prof. Afonso

Objetivo de aprendizagem

Ao final do encontro, o estudante deve ser capaz de aplicar ao projeto os conceitos de Back-End I. Escopo do encontro: Node.js, Models e Controllers.

Estratégia do encontro

Exposição dialogada dos conceitos, alternada com aplicação guiada ao projeto do grupo, e fechamento com verificação do entendimento.

Estrutura do encontro

  1. 1. Por que TDD top-down?
  2. 2. Setup do projeto TypeScript
  3. 3. Setup Jest + ts-jest + supertest
  4. 4. AAA + matchers
  5. 15. Coverage — métrica, não meta
  6. 16. RM-ODP — onde mora o que escrevemos hoje
  7. 17. RF, RN e RNF aplicados ao back-end
  8. 18. Checklist de Estudo

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