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.