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.
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.
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.
# 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.
{
"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:
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"test": "jest",
"test:watch": "jest --watch"
}
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.
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.
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.
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:
| Matcher | Verifica | Quando usar |
|---|---|---|
toBe | Igualdade estrita (===) | Primitivos: number, string, boolean |
toEqual | Igualdade profunda | Objetos e arrays — compara conteúdo, não referência |
toMatchObject | Subset de propriedades | Quando você só quer verificar algumas chaves de um objeto |
toHaveBeenCalledWith | Mock chamado com X | Verificar argumentos passados a uma função mockada |
rejects.toThrow | Promise rejeitada com erro | Casos de erro em código async |
toBeInstanceOf | É instância de uma classe | Para erros customizados (NotFoundError etc.) |
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.
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.
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.
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.
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.
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.
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.
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.
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ó.
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.
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.
import { Router } from 'express';
import { show } from '../controllers/userController';
export const userRoutes = Router();
userRoutes.get('/:id', show);
E o app.ts amarra tudo:
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:
import { app } from './app';
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`API rodando em http://localhost:${port}`);
});
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.
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:
export interface User {
id: number;
name: string;
email: string;
created_at?: Date;
}
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).
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.
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.
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.
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.
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).
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.
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:
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:
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 umifno controller vira uma função pura testável. - Tipar callbacks com
RequestHandlerem 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.
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:
$ 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.
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.
GET /users/:id.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.pg.Pool que gerencia conexões TCP, o middleware que intercepta erros, o servidor Express que serializa JSON. Mecanismos que sustentam o sistema computacional.pg, Jest 29, supertest. A escolha tecnológica concreta.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:
| RF | Descrição | Camada 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.
| RN | Título | Descrição | Vincula |
|---|---|---|---|
| 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).
| RNF | Eixo | Descrição | Vincula |
|---|---|---|---|
| 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 |
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.jsonem modo strict e entendo cada flag. - ✓Configurei
jest.config.tscomts-jestetestMatch: ['**/*.spec.ts']. - ✓Sei usar
supertestpara 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,toMatchObjecte 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
- TypeScript Handbook — documentação oficial: tipos, generics, narrowing, strict mode
- Express.js — Documentation — guia oficial: routing, middleware, error handling
- Jest — Getting Started — referência completa de matchers, mocks e configuração
- supertest — README — exemplos de teste HTTP em Node
- ts-jest — Documentation — integração TypeScript + Jest
- node-postgres (pg) — Documentation — driver oficial do Postgres para Node
- Martin Fowler — Test Driven Development — visão geral pragmática do TDD
- ISO/IEC 10746 — RM-ODP — Reference Model for Open Distributed Processing
- Kent Beck — Test-Driven Development: By Example. Livro fundamental do RED → GREEN → REFACTOR.