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

Testes e Automação

Aula 9 — Refatoração e Requisitos Não Funcionais guiados por métricas

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.

O foco desta aula

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:

RNFO que medeFerramentaMeta razoável
ManutenibilidadeComplexidade e duplicaçãoeslint, jscpdComplexidade ≤ 8 · duplicação < 5%
TestabilidadeCobertura de branchesjest --coverageBranches ≥ 70% · Lines ≥ 80%
ModularidadeCiclos de dependênciamadgeZero ciclos
DesempenhoLatência p99 sob cargaautocannonp99 < 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:

SMELL
📜 Função longa
Mais de ~30 linhas fazendo 3 coisas diferentes. Difícil de nomear, difícil de testar — porque testar uma função que faz tudo exige um teste que simula tudo.
cura: extrair função
SMELL
👯 Código duplicado
O mesmo trecho em 2+ lugares — mesmo que com variável renomeada. Um bug consertado em um lugar continua vivo no outro. É a dívida técnica mais cara.
cura: extrair helper/service
SMELL
🏋️ Controller gordo
Regra de negócio implementada dentro do controller. A consequência direta é que a lógica só é testável via HTTP — lento, frágil e muito verboso.
cura: mover lógica para o service
SMELL
🪜 Ifs aninhados
Três ou mais níveis de indentação (arrow code). A complexidade ciclomática cresce exponencialmente e o número mínimo de testes necessários cresce junto.
cura: guard clauses
SMELL
🔢 Magic numbers
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.
cura: constante nomeada
SMELL
🎭 Nome mentiroso
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.
cura: renomear ou dividir
Regra de ouro da refatoração

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.

A definição formal em uma frase

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çãoIncrementa?Exemplo
if / else if+1if (user.vip)
for / while / do…while+1for (const item of itens)
case (em switch)+1 por casecase 'admin':
catch+1catch (e)
Operador lógico && / ||+1if (a && b) → +2 (o if + o &&)
Operador ternário ?:+1x > 0 ? 'pos' : 'neg'

Exemplo concreto: a função calcularFrete abaixo tem complexidade 7:

services/frete.service.ts — contagem comentada
// 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
1–5saudável
6–10atenção
>10refatore já
Por que o limite recomendado é 10?

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

Bash — instalar plugins
npm install -D eslint eslint-plugin-sonarjs
.eslintrc.json
{
  "plugins": ["sonarjs"],
  "rules": {
    "complexity": ["warn", 8],
    "sonarjs/cognitive-complexity": ["warn", 10]
  }
}
Bash — rodar e ver os avisos
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: um if dentro de um for dentro de um try contribui mais do que três ifs sequenciais. Reflete a dificuldade para um humano entender o código.
Use as duas juntas

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.

Guard clause — antes e depois
// 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.

Condicional → mapa
// 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.

Extrair função
// 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:

  1. Medir antes: anote a complexidade atual (ESLint), duplicação (jscpd) e cobertura (jest).
  2. Suite verde: rode npm test e confirme que tudo passa.
  3. Um refactor pequeno: extraia uma função, adicione uma guard clause, nomeie uma constante.
  4. Suite verde de novo: confirme que o comportamento não mudou.
  5. Medir depois: confirme que a métrica melhorou.
  6. Commit: mensagem clara indicando o que mudou e por quê.
Se quebrou, o suspeito é o último passo

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.

🔴
RED
Escreva o teste primeiro. Ele falha — porque o código nem existe ainda.
🟢
GREEN
Implemente o mínimo para o teste passar. Feio é OK.
🔧
REFACTOR
Melhore o desenho com a rede de segurança verde. É aqui que a métrica cai.

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.

Os testes da aula 6 como infraestrutura

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.

O bug que 100% de Lines esconde

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.

jest.config.ts — threshold mínimo
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;
package.json — scripts
"scripts": {
  "test":     "jest",
  "test:cov": "jest --coverage"
}
Coverage como termômetro, não como vacina

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.

Bash — rodar jscpd
# 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

  1. Rode npx jscpd src e anote o percentual.
  2. Identifique o clone com mais linhas ou mais ocorrências.
  3. Extraia para um helper com nome descritivo.
  4. Atualize os dois (ou mais) lugares para usar o helper.
  5. Rode a suite — suite verde garante que o comportamento não mudou.
  6. Rode jscpd novamente — o percentual deve cair.
O parâmetro --min-tokens

--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).

Bash — detectar ciclos
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
Bash — gerar imagem do grafo (opcional)
# 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 service importando um repository que importa de volta o service destró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 undefined durante a inicialização).
Meta: zero ciclos

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.

Bash — teste de carga básico
# 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:

Saída do autocannon
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.

PercentilSignificadoUso típico em RNF
p50Experiência do usuário típico"p50 < 20 ms"
p95Experiência de 95% dos usuários"p95 < 50 ms"
p99Experiê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 é:

  1. Rode o autocannon antes do refactor e anote os números (p50, p99, req/s).
  2. Faça o refactor.
  3. Rode o autocannon depois e compare.

Refatorar não pode deixar o endpoint mais lento — agora você tem como provar isso com números.

Quando o p99 explode

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ãoPergunta centralConexão com a aula 9
🏢 EnterpriseQual é o propósito e o contexto de negócio?Qualidade como valor entregue ao produto
📋 InformationQue informações circulam no sistema?Métricas como contrato — números documentados
⚙️ ComputationalComo os objetos interagem?Refatoração + RNFs testáveis por interface
🔧 EngineeringComo os componentes se distribuem?npm scripts + hooks locais de qualidade
💻 TechnologyQuais 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 medimosFerramentaStatus
ManutenibilidadeComplexidade ≤ 8 · duplicação < 5%ESLint · jscpd✅ coberto
ConfiabilidadeSuite verde pós-refactorjest✅ coberto
TestabilidadeCobertura com threshold localjest --coverage✅ coberto
Desempenhop99 medido com carga localautocannon✅ coberto
PortabilidadeMétricas reproduzíveis em qualquer máquinanpm scripts→ consequência
Por que usar normas internacionais?

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 complexity e sonarjs/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 coverageThreshold no jest.config para criar portão de qualidade local.
  • Uso jscpd para medir duplicação e identificar os clones mais custosos.
  • Uso madge --circular para detectar e eliminar ciclos de dependência.
  • Rodo autocannon para 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

Inteli Logo