1. De testes a qualidade
Desde a aula 6 vocês têm uma suite de testes funcionando: Jest nos services e helpers, supertest nos controllers, mocks do repository. A suite existe e roda — isso é o ponto de partida, não o destino.
O código funciona. Mas ele é bom? Funcionar é requisito funcional. Ser legível, simples, rápido e sustentável é requisito não funcional (RNF). A diferença entre os dois é que RNF se mede — e é isso que a aula 9 adiciona.
Refatoração guiada por métricas: medir o código com ferramentas npm locais, identificar os piores pontos, refatorar com a suite de testes de guarda e provar a melhora com números. Tudo na sua máquina, zero servidor, zero CI.
RNF se mede, não se opina
Dizer "o código está difícil de manter" é uma opinião. Dizer "a função calcularFrete tem complexidade ciclomática 12 e deveria estar abaixo de 8" é uma métrica — comparável, rastreável e objetiva. A tabela abaixo mostra os quatro eixos de qualidade que vamos medir hoje e as ferramentas que produzem cada número:
| RNF | O que mede | Ferramenta | Meta razoável |
|---|---|---|---|
| Manutenibilidade | Complexidade e duplicação | eslint, jscpd | Complexidade ≤ 8 · duplicação < 5% |
| Testabilidade | Cobertura de branches | jest --coverage | Branches ≥ 70% · Lines ≥ 80% |
| Modularidade | Ciclos de dependência | madge | Zero ciclos |
| Desempenho | Latência p99 sob carga | autocannon | p99 < 100 ms (50 conexões) |
2. Code smells — quando o código pede refactor
Code smell é um sinal de que o código funciona hoje mas vai doer amanhã. Não é um bug — é um padrão estrutural que aumenta o custo de manutenção. O conceito foi popularizado por Martin Fowler no livro Refactoring (1999) e continua sendo o vocabulário padrão da engenharia de software.
Os seis smells mais comuns no contexto do nosso stack Express + TypeScript:
total * 0.9 — o que é 0.9? Desconto? Taxa? IVA? Ninguém lembra em seis meses. O número solto é um contrato não documentado.getUser() que também cria, atualiza e envia e-mail. O nome promete uma coisa, o corpo entrega outra. Leitores confiam no nome e ignoram o corpo.Refatorar = mudar a estrutura sem mudar o comportamento. Quem prova que o comportamento não mudou é a suite de testes: verde antes, refactor pequeno, verde depois. Sem suite verde, não é refactor — é aposta.
3. Complexidade ciclomática
Complexidade ciclomática é a métrica número 1 de manutenibilidade. Criada por Thomas McCabe em 1976, ela conta o número de caminhos independentes que existem dentro de uma função — ou seja, o número mínimo de casos de teste necessários para executar todos os galhos.
3.1 A intuição: a analogia do GPS
Imagine que você vai do Inteli ao seu apartamento de carro. Se não há nenhum cruzamento com decisão (uma rodovia sem saídas), existe exatamente um trajeto. Cada cruzamento com sinal cria mais um caminho possível: você pode virar à esquerda ou seguir em frente. Para "conhecer a cidade inteira" — ou seja, para garantir que seu código funciona em todos os cenários — você precisa percorrer cada trajeto pelo menos uma vez.
No código: cada if que você escreve é uma promessa silenciosa de que alguém vai precisar de mais um teste. Três ifs aninhados? Oito trajetos possíveis, oito testes mínimos.
Complexidade ciclomática = número de arestas − número de nós + 2 no grafo de fluxo da função. Na prática, começa em 1 e você soma 1 para cada decisão.
3.2 Como contar
Comece com 1 (o caminho base) e some +1 para cada uma das estruturas abaixo:
| Construção | Incrementa? | Exemplo |
|---|---|---|
if / else if | +1 | if (user.vip) |
for / while / do…while | +1 | for (const item of itens) |
case (em switch) | +1 por case | case 'admin': |
catch | +1 | catch (e) |
Operador lógico && / || | +1 | if (a && b) → +2 (o if + o &&) |
Operador ternário ?: | +1 | x > 0 ? 'pos' : 'neg' |
Exemplo concreto: a função calcularFrete abaixo tem complexidade 7:
// base = 1
export function calcularFrete(pedido: Pedido) {
if (!pedido.itens.length) throw new Error(); // +1 → 2
let total = 0;
for (const item of pedido.itens) { // +1 → 3
if (item.fragil && item.peso > 10) { // +2 → 5 (if + &&)
total += 25;
} else if (item.peso > 5) { // +1 → 6
total += 12;
}
}
return pedido.vip ? total * 0.9 : total; // +1 → 7
}
// Complexidade = 7 → mínimo de 7 testes para cobrir todos os galhos
McCabe propôs o número 10 como limite máximo em testes empíricos com times industriais. Funções acima de 10 são estatisticamente mais propensas a conter bugs e mais caras de manter. Times maduros costumam usar 8 como alvo interno para ter uma margem.
4. Medindo com ESLint — feedback em segundos
A regra complexity do ESLint implementa exatamente o algoritmo de McCabe. Com um arquivo de configuração e um npx, você transforma a métrica em aviso automático no terminal — e no editor enquanto digita.
Instalação e configuração
npm install -D eslint eslint-plugin-sonarjs
{
"plugins": ["sonarjs"],
"rules": {
"complexity": ["warn", 8],
"sonarjs/cognitive-complexity": ["warn", 10]
}
}
npx eslint src/
# saída típica:
src/services/frete.service.ts
12:1 warning 'calcularFrete' has a complexity of 12. Maximum allowed is 8
40:3 warning Reduce Cognitive Complexity from 14 to 10
✖ 2 problems (0 errors, 2 warnings) — sua lista de refactor está pronta
Duas métricas irmãs
O ESLint oferece duas métricas complementares de complexidade:
complexity(ciclomática) — conta caminhos que a máquina percorre. Reflete o número mínimo de testes. É a métrica de McCabe.sonarjs/cognitive-complexity(cognitiva) — criada pela SonarSource, pesa o aninhamento de estruturas: umifdentro de umfordentro de umtrycontribui mais do que trêsifs sequenciais. Reflete a dificuldade para um humano entender o código.
Ciclomática alta significa mais testes necessários. Cognitiva alta significa mais dificuldade de leitura. Uma função pode ter ciclomática baixa e cognitiva alta (muitos ternários em linha) — por isso use as duas. O limite de cada uma é da equipe; os valores 8 e 10 são pontos de partida, não dogmas.
5. Refatoração na prática
Refatoração não é "reescrever o código do zero". É um conjunto de transformações pequenas e seguras, cada uma com nome, intenção e efeito conhecido. Martin Fowler catalogou mais de 70 refactorings; aqui estão os quatro mais úteis para reduzir complexidade ciclomática:
5.1 Técnicas
Guard clause / Early return
Em vez de aninhar o caso principal dentro de um if, trate o caso inválido no início e retorne imediatamente. Desfaz o aninhamento e torna o fluxo principal mais fácil de ler.
// ANTES — complexidade 6, três níveis de aninhamento
function desconto(user, total) {
if (user) {
if (user.vip) {
if (total > 100) { return total * 0.8; }
else { return total * 0.9; }
} else if (total > 100) { return total * 0.95; }
}
return total;
}
// DEPOIS — guard clause, complexidade 4, lê de cima para baixo
const VIP_ALTO = 0.8, VIP = 0.9, ALTO = 0.95; // constante nomeada mata magic number
function desconto(user, total) {
if (!user) return total; // guard
if (user.vip) return total > 100 ? total * VIP_ALTO : total * VIP;
return total > 100 ? total * ALTO : total;
}
Substituir condicional por mapa (lookup table)
Quando uma série de if/else if mapeia um valor para outro, um objeto ou Map elimina todos os galhos de uma vez — e é extensível sem tocar na lógica.
// ANTES — complexidade +3
function taxa(tipo: string) {
if (tipo === 'padrao') return 0.05;
if (tipo === 'expresso') return 0.12;
if (tipo === 'vip') return 0.00;
return 0.05;
}
// DEPOIS — complexidade 1 (zero ifs)
const TAXAS: Record<string, number> = {
padrao: 0.05, expresso: 0.12, vip: 0.00,
};
const taxa = (tipo: string) => TAXAS[tipo] ?? 0.05;
Extrair função
Quando um trecho de código tem um propósito identificável, extraia para uma função com nome descritivo. Reduz o tamanho da função original e aumenta a legibilidade de ambas.
// ANTES — função de 40 linhas que faz validação + cálculo + formatação
function processarPedido(input) {
// 15 linhas de validação
// 15 linhas de cálculo de frete
// 10 linhas de formatação da resposta
}
// DEPOIS — cada responsabilidade tem nome
function validarPedido(input) { /* ... */ }
function calcularFrete(pedido) { /* ... */ }
function formatarResposta(pedido) { /* ... */ }
function processarPedido(input) {
const pedido = validarPedido(input);
calcularFrete(pedido);
return formatarResposta(pedido);
}
5.2 O processo — passos curtos
O erro mais comum é fazer um refactor grande e descobrir que a suite quebrou em dez lugares ao mesmo tempo. O caminho certo é uma sequência de passos mínimos:
- Medir antes: anote a complexidade atual (ESLint), duplicação (jscpd) e cobertura (jest).
- Suite verde: rode
npm teste confirme que tudo passa. - Um refactor pequeno: extraia uma função, adicione uma guard clause, nomeie uma constante.
- Suite verde de novo: confirme que o comportamento não mudou.
- Medir depois: confirme que a métrica melhorou.
- Commit: mensagem clara indicando o que mudou e por quê.
Passos curtos têm exatamente essa vantagem: quando a suite falha, você sabe exatamente qual foi a mudança que causou o problema. Não há como saber isso se você fez 30 mudanças antes de rodar o teste.
6. Red · Green · Refactor — vivendo no 3º passo
TDD (Test-Driven Development) é o ciclo de Kent Beck: escreva o teste primeiro (Red), implemente o mínimo para passar (Green) e melhore o código com a rede de segurança ativa (Refactor). Vocês já conhecem as duas primeiras etapas. O foco da aula 9 é o terceiro passo — o que o time costuma pular.
O passo que todo mundo pula
Na pressa de entregar a próxima feature, o ciclo vira Red → Green → próxima feature. O código vai ficando mais complexo a cada sprint. O débito acumula até o desenvolvimento travar.
Na aula 9, o REFACTOR deixa de ser opcional: ele tem métrica (complexidade), tem meta (≤ 8) e tem entrega (tabela de métricas antes/depois no MR). A suite de testes escritos desde a aula 6 é exatamente a rede de segurança que torna esse passo possível.
Cada teste que vocês escreveram desde a aula 6 é uma trava contra regressão. Hoje, cada refactor roda contra eles: comportamento igual, estrutura melhor, número menor. É o investimento dos testes se pagando.
7. Cobertura — boa métrica, péssima meta
Coverage mede quais partes do código foram executadas durante a suite. É uma ferramenta de diagnóstico poderosa — mas fixar 100% de cobertura como meta é um dos erros mais comuns em times de desenvolvimento.
7.1 As 4 métricas de cobertura
O Jest gera quatro números para cada arquivo. Eles medem coisas diferentes e têm pesos diferentes:
| Métrica | O que conta | Exemplo: só desconto({vip:false}, 100) rodou |
Confiabilidade |
|---|---|---|---|
| Lines | Linhas físicas tocadas ao menos 1× | 3/3 = 100% | ⭐ mais fácil de enganar |
| Statements | Instruções executadas (1 linha pode ter várias) | 2/3 = 67% | ⭐⭐ |
| Functions | Funções chamadas ao menos 1× | 1/1 = 100% | ⭐⭐ |
| Branches | Cada galho de if/else, ternário, ??, && |
1/2 = 50% | ⭐⭐⭐ mais honesta |
A mesma função, o mesmo único teste, produz 100% em Lines e Functions mas apenas 50% em Branches. O galho user.vip = true nunca foi testado — a lógica de desconto VIP pode estar completamente errada e o relatório ainda mostra verde em duas das quatro métricas.
Se o galho user.vip = true nunca foi testado, a lógica de desconto VIP pode estar errada desde o início. O relatório mostra verde em Lines e Functions. Use Branches como métrica primária — ela obriga você a cobrir cada decisão, não só passar pela linha.
7.2 Threshold local no jest.config
Em vez de olhar o relatório manualmente, configure um portão de qualidade automático: se a cobertura cair abaixo do limite, npm run test:cov falha na sua máquina antes do commit.
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'],
coverageThreshold: {
global: {
branches: 70, // Branches: o que mais importa
lines: 80, // Lines: threshold mais generoso
functions: 80,
statements: 80,
},
},
};
export default config;
"scripts": {
"test": "jest",
"test:cov": "jest --coverage"
}
Uma linha coberta sem assert não testa nada. Use cobertura para achar buracos — partes do código que nunca são exercidas. Não use como troféu ou como meta de sprint. Fixar 100% incentiva testes decorativos que executam o código sem verificar comportamento.
8. jscpd — detecção de duplicação
O jscpd (JavaScript Copy/Paste Detector) acha trechos duplicados por tokens, não por texto. Isso significa que ele detecta cópias mesmo quando a variável foi renomeada — o padrão estrutural é o mesmo.
# instalar globalmente ou rodar com npx
npx jscpd src --min-tokens 30
# saída típica:
Clone found (typescript):
- src/controllers/user.controller.ts [12:1 - 34:2]
src/controllers/product.controller.ts [9:1 - 31:2]
──────────────┬────────┬────────────────┬──────────
typescript │ 28 fls │ 6.4% duplicado │ 5 clones
# meta: abaixo de 5%
Por que duplicação é o smell mais caro
Quando o mesmo trecho existe em dois lugares, um bug corrigido em um lugar continua vivo no outro. Com o tempo, as cópias divergem — cada uma recebe ajustes independentes — e rastrear qual versão é a "correta" torna-se impossível. Extrair o trecho para um helper elimina a dívida e garante que a correção ocorra em um único lugar.
Fluxo de trabalho
- Rode
npx jscpd srce anote o percentual. - Identifique o clone com mais linhas ou mais ocorrências.
- Extraia para um helper com nome descritivo.
- Atualize os dois (ou mais) lugares para usar o helper.
- Rode a suite — suite verde garante que o comportamento não mudou.
- Rode
jscpdnovamente — o percentual deve cair.
--min-tokens 30 ignora trechos muito curtos (imports comuns, declarações simples). Ajuste conforme o nível de ruído: 20 tokens detecta mais coisas mas produz falsos positivos; 50 tokens é mais conservador. O padrão do jscpd é 50.
9. madge — grafo de dependências e ciclos
O madge constrói um grafo de importações do projeto e detecta dependências circulares: situações onde o módulo A importa B que importa A (direta ou indiretamente).
npx madge --circular src
# saída quando há ciclo:
✖ Found 1 circular dependency!
1) services/order.service.ts > repositories/order.repository.ts
> services/order.service.ts
# requer graphviz instalado
npx madge --image graph.svg src
Por que ciclos são um problema
Um ciclo de dependências significa que dois módulos não podem ser carregados ou testados de forma independente — cada um precisa do outro para existir. Na prática:
- Testes impossíveis: para testar A em isolamento você precisa de B; para instanciar B, precisa de A. O mock vira um labirinto.
- Camadas vazando: um
serviceimportando umrepositoryque importa de volta oservicedestrói a arquitetura em camadas. - Builds instáveis: bundlers modernos lidam razoavelmente com ciclos, mas o comportamento em runtime pode ser imprevisível (um dos módulos pode ser
undefineddurante a inicialização).
Ao contrário da complexidade (onde 8 é razoável), ciclos de dependência não têm "tolerância aceitável". Um ciclo é sempre um sinal de que a separação de responsabilidades foi violada. A meta é zero e deve ser mantida.
Como resolver um ciclo
A causa mais comum é um módulo que cresceu e assumiu responsabilidades que pertencem a outra camada. A solução é extrair a parte compartilhada para um terceiro módulo sem dependências — um helper, um tipo, um evento — que ambos possam importar sem criar o ciclo.
10. autocannon — RNF de desempenho
O autocannon é um cliente HTTP de carga escrito em Node.js. Ele dispara centenas de requisições simultâneas contra um endpoint e reporta estatísticas de latência e throughput. Tudo na sua máquina, sem precisar de infraestrutura de testes de carga.
# app de pé na porta 3000: npm run dev
npx autocannon -c 50 -d 10 http://localhost:3000/api/products
# -c 50 → 50 conexões simultâneas
# -d 10 → duração de 10 segundos
Saída típica:
Running 10s test @ http://localhost:3000/api/products
50 connections
┌─────────┬───────┬───────┬────────┬────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │
│ Latency │ 4 ms │ 9 ms │ 38 ms │ 61 ms │
└─────────┴───────┴───────┴────────┴────────┘
Req/Sec média: 4.512
45k requests in 10.02s
→ RNF testável: "p99 < 100 ms com 50 conexões" — passou ✓
Percentis, não média
A média esconde os piores casos. Se 98% das requisições levam 5 ms e 2% levam 5 segundos, a média fica em ~105 ms — um número que parece aceitável mas que representa milhares de usuários com experiência péssima.
| Percentil | Significado | Uso típico em RNF |
|---|---|---|
| p50 | Experiência do usuário típico | "p50 < 20 ms" |
| p95 | Experiência de 95% dos usuários | "p95 < 50 ms" |
| p99 | Experiência do usuário azarado | "p99 < 100 ms" |
RNF de desempenho sério se escreve em percentil: "p99 < 100 ms com 50 conexões simultâneas". É um critério de aceitação verificável — o autocannon confirma ou reprova em 10 segundos.
Baseline antes, prova depois
O fluxo correto é:
- Rode o autocannon antes do refactor e anote os números (p50, p99, req/s).
- Faça o refactor.
- Rode o autocannon depois e compare.
Refatorar não pode deixar o endpoint mais lento — agora você tem como provar isso com números.
Latência alta quase sempre é a query. Se o p99 disparar após uma mudança, volte ao EXPLAIN ANALYZE no PostgreSQL — um índice faltando aparece no percentil 99 antes de qualquer outro sinal. O autocannon revela o sintoma; o banco de dados geralmente guarda a causa.
11. RM-ODP e ISO/IEC 25010 — qualidade nas 5 visões
O RM-ODP (Reference Model for Open Distributed Processing — ISO/IEC 10746) organiza a descrição de um sistema em cinco visões complementares. Cada visão responde a uma pergunta diferente sobre o sistema.
| Visão | Pergunta central | Conexão com a aula 9 |
|---|---|---|
| 🏢 Enterprise | Qual é o propósito e o contexto de negócio? | Qualidade como valor entregue ao produto |
| 📋 Information | Que informações circulam no sistema? | Métricas como contrato — números documentados |
| ⚙️ Computational | Como os objetos interagem? | Refatoração + RNFs testáveis por interface |
| 🔧 Engineering | Como os componentes se distribuem? | npm scripts + hooks locais de qualidade |
| 💻 Technology | Quais tecnologias concretas? | Jest, ESLint, jscpd, madge, autocannon |
A aula 9 atravessa as cinco visões pelo eixo de qualidade mensurável: começamos pelo por que (Enterprise — o código sustentável é um ativo do produto), passamos pelos contratos em número (Information), descemos para os objetos e suas interfaces (Computational), chegamos à orquestração local com npm (Engineering) e terminamos nas ferramentas concretas (Technology).
ISO/IEC 25010 — os 8 eixos de qualidade
A norma ISO/IEC 25010 define as características de qualidade de software. Os quatro eixos que cobrimos hoje têm correspondência direta:
| Característica (ISO 25010) | O que medimos | Ferramenta | Status |
|---|---|---|---|
| Manutenibilidade | Complexidade ≤ 8 · duplicação < 5% | ESLint · jscpd | ✅ coberto |
| Confiabilidade | Suite verde pós-refactor | jest | ✅ coberto |
| Testabilidade | Cobertura com threshold local | jest --coverage | ✅ coberto |
| Desempenho | p99 medido com carga local | autocannon | ✅ coberto |
| Portabilidade | Métricas reproduzíveis em qualquer máquina | npm scripts | → consequência |
Normas como ISO/IEC 25010 e RM-ODP existem para criar vocabulário comum entre times, clientes e auditores. "O código tem manutenibilidade ruim" é uma opinião. "O código não atende ISO/IEC 25010 §8.5 (Manutenibilidade) — complexidade ciclomática média de 14.2 contra o limite de 8 da equipe" é uma afirmação verificável. Na vida profissional, você vai precisar justificar decisões técnicas para pessoas que não leem código — números e normas são a linguagem certa.
12. Checklist de estudo
Marque conforme dominar:
- ✓Sei identificar os 6 code smells e explicar o custo de cada um.
- ✓Consigo calcular a complexidade ciclomática de uma função à mão contando os operadores de decisão.
- ✓Configuro ESLint com as regras
complexityesonarjs/cognitive-complexity. - ✓Aplico guard clauses para reduzir aninhamento e complexidade ciclomática.
- ✓Substituo cadeias de if/else por lookup table (objeto ou Map).
- ✓Sei diferenciar as 4 métricas de cobertura e explico por que Branches é a mais honesta.
- ✓Configuro
coverageThresholdno jest.config para criar portão de qualidade local. - ✓Uso
jscpdpara medir duplicação e identificar os clones mais custosos. - ✓Uso
madge --circularpara detectar e eliminar ciclos de dependência. - ✓Rodo
autocannonpara medir latência e sei interpretar p50/p95/p99. - ✓Sei escrever um RNF de desempenho em formato testável: "p99 < X ms com Y conexões".
- ✓Executo o ciclo completo: medir → refatorar → medir → provar com suite verde.
- ✓Consigo associar cada métrica ao eixo de qualidade correspondente da ISO/IEC 25010.
- ✓Produzo uma tabela de métricas antes/depois para documentar um refactor no MR.
Referências
- ESLint — regra
complexity— documentação oficial da regra ciclomática - eslint-plugin-sonarjs — plugin com cognitive-complexity e outros
- jscpd — JavaScript Copy/Paste Detector
- madge — grafo de dependências e detecção de ciclos
- autocannon — cliente HTTP de carga em Node.js
- Jest — coverageThreshold — configuração de portão de cobertura
- Martin Fowler — Refactoring — catálogo de refactorings (2ª ed. usa JavaScript)
- Martin Fowler — Code Smell — definição e história do conceito
- ISO/IEC 25010 — características de qualidade de produto de software
- ITU-T X.902 / ISO/IEC 10746 — RM-ODP — as 5 visões de um sistema distribuído