Técnicas de teste e edge cases

Resumo

Bom teste não nasce de inspiração — nasce de método. Equivalence partitioning divide o domínio de entrada em classes que o sistema trata do mesmo jeito, para que um representante de cada classe baste. Boundary value analysis ataca as fronteiras dessas classes, porque é ali — no >= que devia ser > — que o off-by-one se esconde. Decision tables e state-transition testing cobrem o que a partição sozinha não alcança: combinações de regras de negócio e sistemas com memória, onde a mesma entrada produz efeitos diferentes conforme o estado. Por cima de tudo isso roda o checklist de edge cases — vazio, null, overflow, unicode, timezone, concorrência — a lista de tudo que já quebrou em produção, aplicada a cada parâmetro como uma pergunta hostil: “onde isso quebra?”

A pergunta que trava todo mundo na frente do editor não é “como eu testo isso?” — é “que casos eu escrevo?“. Sem método, você cai num de dois extremos: ou escreve três testes do caminho feliz e dá por encerrado, ou tenta testar valores infinitos e nunca termina. As duas saídas são ruins.

Esta nota é o catálogo de técnicas que transforma um espaço de entradas potencialmente infinito num conjunto pequeno e justificável de casos. A ideia central é velha e provada: você não consegue testar tudo, então teste de forma a maximizar a chance de pegar um bug por caso escrito.

Vídeo — Boundary Value Analysis, Equivalence Partitioning e Decision Table

Test-o-blog: “05: Boundary Value Analysis, Equivalence class Partitioning and Decision Table” (6:31) — percorre as três técnicas em sequência com exemplos numéricos, reforçando por que decision table entra quando EP/BVA sozinhas não bastam.

A mentalidade certa

O bom testador não pensa como o autor do código (“será que funciona?”). Pensa como um adversário procurando a brecha (“onde isso quebra?”). Cada técnica abaixo é uma maneira sistemática de fazer essa pergunta hostil.

Caixa-preta × caixa-branca

Antes de escolher casos, decida de onde você os deriva.

  • Caixa-preta (black-box): você deriva os casos do contrato/spec — o que a função promete fazer, dadas quais entradas. Não olha o código por dentro. Equivalence partitioning, boundary value analysis, decision tables e state-transition são todas técnicas black-box.
  • Caixa-branca (white-box): você deriva os casos da estrutura interna — quais ramos (if/else), laços e caminhos existem no código. O objetivo é exercer os caminhos. É aqui que mora a conversa de cobertura, que aprofundo em 12 - Coverage e mutation testing.

As duas se complementam

Black-box pega o que o código deveria fazer e esqueceu. White-box pega o que o código faz e não deveria (um if a mais, um caminho não testado). Um time maduro usa as duas: deriva casos da spec e depois checa quais ramos ficaram descobertos.

Na prática do dia a dia (e da maioria das notas desta trilha), você trabalha majoritariamente black-box. As técnicas a seguir são o coração disso.

Equivalence Partitioning — partição de equivalência

A premissa: o sistema trata grupos inteiros de entradas exatamente do mesmo jeito. Se a faixa 1..17 é tratada como “menor de idade” e 18..120 como “adulto”, não adianta testar 5, 6, 7, 8… — eles caem todos na mesma classe. Testar um representante de cada classe é, por hipótese, equivalente a testar qualquer outro valor dela.

A definição ISTQB

Partição de equivalência divide os dados em partições com base na expectativa de que todos os elementos de uma dada partição são processados da mesma forma pelo objeto sob teste. Se um caso que testa um valor da partição detecta um defeito, esse defeito deveria ser detectado por qualquer outro valor da mesma partição.

O método tem três passos:

  1. Identifique o domínio de entrada.
  2. Particione-o em classes de equivalência — incluindo as inválidas (entradas que o sistema deve rejeitar).
  3. Escolha um representante de cada classe.

Cadastro com campo "idade" aceitando 18 a 120

ClasseFaixaRepresentanteEsperado
Inválida (baixa)menor que 1810rejeita
Válida18 a 12042aceita
Inválida (alta)maior que 120200rejeita
Inválida (não-número)“abc""abc”rejeita

Quatro casos cobrem um domínio de bilhões de valores possíveis. Esse é o ganho.

flowchart LR
    subgraph Inválida_baixa["Classe inválida (menor que 18)"]
        A["... -5 ... 10 ... 17"]
    end
    subgraph Válida["Classe válida (18 a 120)"]
        B["18 ... 42 ... 120"]
    end
    subgraph Inválida_alta["Classe inválida (maior que 120)"]
        C["121 ... 200 ..."]
    end
    A -->|"representante: 10"| R1["1 caso"]
    B -->|"representante: 42"| R2["1 caso"]
    C -->|"representante: 200"| R3["1 caso"]

Lead-in: o diagrama mostra três classes de equivalência do campo “idade” e o representante escolhido de cada uma.

Leitura do diagrama: o domínio inteiro (de menos infinito a mais infinito) colapsa em três grupos. Dentro de cada grupo, qualquer valor é “tão bom quanto” outro para o teste — então pegamos um só. Repare que a classe válida é uma só, mas as inválidas são duas (abaixo e acima): erro comum é testar só uma borda do válido e esquecer que existe inválido dos dois lados.

Partição multidimensional: quando há mais de um parâmetro

O exemplo acima particiona um parâmetro. Uma função real quase sempre tem vários — e aí a pergunta muda: você particiona cada parâmetro separadamente (e testa um representante de cada classe, isoladamente) ou as combinações entre parâmetros (produto cartesiano das classes)?

A resposta prática do ISTQB syllabus é: comece particionando cada parâmetro de forma independente — é o que o 4n de BVA também assume (hipótese de falha única, uma variável de cada vez). Combinações entre partições só entram quando há motivo de negócio para achar que elas interagem (como no exemplo de bloqueio de login mais abaixo, em que “senha correta” e “conta bloqueada” interagem de propósito). Combinar partições sem motivo é a mesma armadilha do worst-case testing: o número de casos explode (k classes em n parâmetros dão k^n combinações) muito mais rápido do que o número de bugs reais de interação.

Boundary Value Analysis — análise de valor limite

Partição de equivalência diz onde testar. Análise de valor limite diz que bugs moram nas bordas. E moram mesmo: o programador escreve if (idade >= 18) ou if (idade > 18)? <= ou <? É na fronteira entre duas classes que o off-by-one se esconde.

Myers, The Art of Software Testing

“Test cases that explore boundary conditions have a higher payoff than cases that do not.” Myers vai além: BVA “requer um grau de criatividade” e “é mais um estado de espírito do que qualquer outra coisa”. Não é receita de bolo — é o hábito de sempre olhar para a beira do precipício.

A receita de Myers para uma faixa: teste o mínimo, logo acima do mínimo, um valor nominal, logo abaixo do máximo, o máximo, e os valores logo fora dos dois extremos. Na prática condensada: para um limite N, teste N-1, N e N+1.

flowchart LR
    L1["17<br/>(fora, inválido)"] --> L2["18<br/>(limite inferior)"]
    L2 --> L3["19<br/>(dentro)"]
    L3 --> M["..."]
    M --> L4["119<br/>(dentro)"]
    L4 --> L5["120<br/>(limite superior)"]
    L5 --> L6["121<br/>(fora, inválido)"]

    style L2 fill:#2d6a4f,color:#fff
    style L5 fill:#2d6a4f,color:#fff
    style L1 fill:#9d0208,color:#fff
    style L6 fill:#9d0208,color:#fff

Lead-in: a mesma faixa de idade (18 a 120), agora vista pela lente das fronteiras.

Leitura do diagrama: os valores em verde são os limites válidos (18, 120); os em vermelho são os vizinhos inválidos (17, 121). É exatamente nesse vai-e-vem 17 → 18 → 19 e 119 → 120 → 121 que >= vira > por descuido. Off-by-one mora aqui. Repare: BVA não substitui a partição de equivalência — ela mira nas bordas das classes que a partição já desenhou.

Bordas não-óbvias

Limite não é só número. Para uma string: vazia, 1 caractere, tamanho máximo, máximo+1. Para uma lista: vazia, 1 elemento, no limite de capacidade. Para uma data: virada de mês, virada de ano, 29/fev. Toda dimensão tem uma borda — caçá-las é o estado de espírito do Myers.

Worst-case testing: quando uma fronteira não basta

BVA simples assume a hipótese de falha única: testa uma variável no limite de cada vez, mantendo as outras em valor nominal. Para uma função com n variáveis, isso gera 4n + 1 casos. Mas e se o bug só aparecer quando duas fronteiras falham ao mesmo tempo — por exemplo, o desconto máximo aplicado no mesmo pedido que atinge o limite de itens do carrinho?

Essa é a pergunta que o worst-case testing responde: em vez de variar uma fronteira por vez, ele combina os cinco valores de BVA (mínimo, logo-acima-do-mínimo, nominal, logo-abaixo-do-máximo, máximo) de cada variável com os de todas as outras, gerando 5^n casos. É caro — cresce exponencial com o número de variáveis — mas pega a classe de bug que a hipótese de falha única esconde.

Existe um meio-termo mais usado na prática: o robustness testing (ou “robust worst-case”), que soma às fronteiras válidas os valores fora do domínio (logo abaixo do mínimo, logo acima do máximo) para verificar que o sistema rejeita com uma mensagem de erro decente, em vez de travar. Combinado ponto a ponto, isso produz 7^n casos no worst-case robusto — geralmente demais para rodar à mão, mas é exatamente o espaço de casos que fuzzing orientado por fronteira cobre de forma automática (mais sobre isso adiante).

Quando vale o esforço extra

Para uma função com 2-3 variáveis, worst-case testing completo é viável manualmente. Acima disso, o custo de escrever e manter 5^n/7^n casos supera o ganho — a resposta prática é combinar BVA simples nas fronteiras mais críticas com pairwise testing (cobrir toda combinação de pares de valores, não todas as combinações) para reduzir o espaço sem abrir mão de pegar interações de duas variáveis, que são a maioria dos bugs de combinação segundo estudos de interação de falhas.

Em uma linha

BVA simples testa uma fronteira por vez; worst-case e robustness testing combinam fronteiras de várias variáveis — use-os quando houver motivo real para achar que elas interagem, não por padrão.

Decision Tables — tabelas de decisão

Quando o comportamento depende de combinações de condições, partição e fronteira não dão conta sozinhas. É o território de regras de negócio: “se cliente premium E pedido acima de 500 E primeira compra → frete grátis + cupom”. Três condições booleanas geram 2³ = 8 combinações. A tabela de decisão lista as condições, as combinações e a ação resultante de cada uma.

ISTQB

Tabelas de decisão testam a implementação de requisitos que especificam como diferentes combinações de condições resultam em diferentes desfechos. São uma forma eficaz de registrar lógica complexa, como regras de negócio.

Comissão de vendas (gancho com 09 - TDD na prática)

Regra: comissão de 10% se vendeu acima da meta; +5% de bônus se o cliente é novo; nada se a venda foi cancelada.

#Acima da meta?Cliente novo?Cancelada?Ação
1simsimnão15%
2simnãonão10%
3nãosimnão0% (só bônus não vale sem base)
4nãonãonão0%
5**sim0% (cancelada zera tudo)

A linha 5 usa * (“don’t care”): quando a venda é cancelada, as outras condições não importam. Isso colapsa 4 combinações em uma só regra — exatamente o que a tabela revela e o que um teste cego por amostragem perderia.

A tabela é também uma ferramenta de descoberta de requisito: ao preenchê-la, você costuma achar combinações que ninguém especificou (“e se for cliente novo numa venda cancelada?”). Cada linha vira um caso de teste; cada combinação não-coberta vira uma pergunta para o PO.

Como colapsar a tabela sem perder cobertura

O exemplo acima já nasceu colapsado — mas vale ver o processo, porque é isso que separa uma tabela de decisão de uma lista de casos qualquer. Comece pela tabela completa: três condições booleanas dão 2³ = 8 linhas, uma para cada combinação de sim/não. Depois, procure linhas cuja ação é idêntica independente de uma das condições — essa condição vira *. No exemplo da comissão, as quatro combinações com “Cancelada = sim” (sim/sim, sim/não, não/sim, não/não para as duas primeiras condições) dão sempre a mesma ação (0%), então colapsam nas 4 → 1 linha. O algoritmo é mecânico: agrupe linhas idênticas exceto por uma condição, substitua a condição divergente por *, repita até não haver mais grupo para colapsar. O ganho não é só menos digitação — é a prova visual de que uma condição realmente não influencia o resultado; se ao tentar colapsar você descobrir que duas linhas do “grupo” na verdade têm ações diferentes, achou uma regra de negócio que o requisito original não deixou clara.

Em uma linha

Colapsar uma tabela de decisão não é atalho — é a prova de que uma condição não importa; quando o colapso falha, você achou uma regra escondida.

State-Transition Testing — teste de transição de estado

Sistemas com memória — um pedido, uma sessão de login, um caixa eletrônico — não dependem só da entrada atual, mas do estado em que estão. A mesma ação (“pagar”) tem efeito diferente se o pedido está criado ou já está enviado. Aqui a técnica é modelar os estados e testar as transições — as válidas e, principalmente, as inválidas.

ISTQB

Um diagrama de estados modela o comportamento de um sistema mostrando seus estados possíveis e as transições válidas. Uma transição é iniciada por um evento, que pode ser qualificado por uma condição de guarda.

stateDiagram-v2
    [*] --> Criado
    Criado --> Pago: pagar
    Criado --> Cancelado: cancelar
    Pago --> Enviado: despachar
    Pago --> Reembolsado: reembolsar
    Enviado --> Entregue: confirmar
    Entregue --> [*]
    Cancelado --> [*]
    Reembolsado --> [*]

Lead-in: ciclo de vida de um pedido, com os estados e as transições legítimas.

Leitura do diagrama: cada seta é uma transição que deve funcionar — teste todas. Mas o ouro do state-transition está no que não aparece no diagrama: as transições inválidas. “Despachar um pedido Criado (ainda não pago)” não tem seta — então o sistema deve recusar essa ação. “Cancelar um pedido Entregue” também não existe. Cada transição ausente é um caso de teste negativo.

Quanto é “suficiente”? N-switch coverage

Testar cada seta do diagrama uma vez cobre as transições individuais — mas não pega o bug que só aparece numa sequência. Um pedido pago-e-reembolsado-no-mesmo-segundo pode deixar o estoque inconsistente mesmo que pagar e reembolsar funcionem perfeitamente isolados. Chow formalizou os níveis de cobertura de sequência como n-switch coverage, onde n é o número de transições consecutivas testadas menos um:

  • 0-switch coverage: cada transição individual é testada pelo menos uma vez — é o que o diagrama acima já cobre.
  • 1-switch coverage: todo par de transições consecutivas válidas é testado — por exemplo, pagar → despachar e despachar → confirmar como sequências, não isoladas.
  • N-switch coverage: sequências de n+1 transições consecutivas. Na prática, o syllabus raramente exige além de 1-switch ou 2-switch — o custo de enumerar sequências cresce rápido.

Em uma linha

0-switch testa cada seta do diagrama; 1-switch testa pares de setas em sequência — a maioria dos bugs de “estado sujo” só aparece na sequência, nunca na transição isolada.

O checklist de edge cases

As técnicas acima são o esqueleto. O checklist abaixo é o músculo — a lista que você roda mentalmente sobre cada entrada do código. Não é decoreba; é a memória institucional de tudo que já quebrou em produção. Cada categoria vem com o por quê.

CategoriaExemploPor quê pega bug
Input vazio"", [], {}Código que faz lista[0] ou divide por len estoura no vazio
Null / undefinednull, None, nilO NPE mais caro da história; “bilhão de dólares de erro”
Valores no limite0, -1, MAX_INT, MIN_INTOff-by-one e o zero que zera a divisão
OverflowMAX_INT + 1, soma de saldosInteiro estoura e vira negativo silenciosamente
Unicodeemoji, RTL (árabe/hebraico), combining charslength mente; "👨‍👩‍👧" tem 1 “caractere” e 11 code units
Timezone / horário de verão23:30 na virada do DSTUma hora some ou repete; agendamentos disparam errado
Datas29/fev, fim de mês, ano bissexto31 + 1 mês não é “32”; fevereiro tem 28 ou 29
Duplicatasmesmo item duas vezes na listaSet engole, regra de negócio talvez não
Ordem inversa / aleatóriaentrada já ordenada, ao contrário, embaralhadaAlgoritmo que assumiu ordem quebra; pior caso de sort
Tamanhos extremos1 elemento × milhõesO que passa com 10 estoura memória com 10 milhões
Caracteres especiaisaspas, '; DROP TABLE, <script>Injection (SQL/HTML); escaping mal feito
Erros de redetimeout, HTTP 500, conexão fechadaO happy path da rede é mentira; ela falha sempre
Recursos esgotadospool de conexão cheio, disco/memória cheiosSob carga, o que sobra acaba — e o erro vem feio
Concorrência + falha parcialduas threads, falha no meio da transaçãoRace condition; estado meio-gravado sem rollback

Como usar o checklist

Não aplique os 14 itens cegamente a tudo. Para cada parâmetro da função, pergunte: “ele é string? então: vazia, null, unicode, longa demais. É número? então: zero, negativo, MAX, overflow. É data? então: 29/fev, fim de mês, timezone.” O checklist é um gerador de perguntas, não uma cota de testes.

Aplicando o checklist a um parâmetro email: string

Passe o parâmetro pelas categorias que se aplicam a string e veja quantos casos relevantes o checklist já sugere sem nenhuma criatividade extra:

Categoria do checklistValor de testeO que verifica
Input vazio""Rejeita, ou trata como “sem e-mail”?
Null / undefinednullNão estoura NPE antes da validação
Unicode"tëst@exämple.com", domínio com IDNNormalização/validação não quebra em acento
Caracteres especiais"'; DROP TABLE--@x.com"Escaping correto na camada de persistência
Tamanhos extremose-mail com 300+ caracteresRespeita o limite do RFC 5321 (254 chars) ou trunca silenciosamente?
Duplicatasmesmo e-mail cadastrado 2x, com maiúsculas diferentesFoo@x.com e foo@x.com colidem ou não?

Seis linhas, um parâmetro, nenhuma delas inventada — cada uma é uma categoria do checklist aplicada literalmente. É esse mapeamento mecânico que transforma “tenta lembrar dos edge cases” em “roda a lista”.

Testar o caminho de erro — não só o happy path

Aqui está o conselho que mais economiza noites de plantão: o caminho de erro é código que também roda — e quase ninguém testa. O happy path é o que você imagina ao escrever a função. O caminho de erro é o catch, o rollback, a compensação, o retry. É justamente onde o código foi escrito com menos atenção e exercitado com menos frequência.

flowchart TD
    Start["Requisição chega"] --> Valid{"Entrada<br/>válida?"}
    Valid -->|sim| Process["Processa pedido"]
    Valid -->|não| Err1["Retorna 400<br/>(testar!)"]
    Process --> Save{"Gravou no<br/>banco?"}
    Save -->|sim| Happy["200 OK<br/>(happy path)"]
    Save -->|não| Rollback["Rollback +<br/>compensação<br/>(testar!)"]
    Rollback --> Err2["Retorna 500<br/>(testar!)"]

    style Happy fill:#2d6a4f,color:#fff
    style Err1 fill:#9d0208,color:#fff
    style Err2 fill:#9d0208,color:#fff
    style Rollback fill:#bb3e03,color:#fff

Lead-in: o fluxo de uma requisição, com o caminho feliz (verde) e os caminhos de erro (vermelho/laranja) lado a lado.

Leitura do diagrama: o caminho verde é o único que a maioria testa. Mas para chegar nele, a requisição passou por uma validação (que pode rejeitar) e por uma gravação (que pode falhar). Cada losango é uma bifurcação, e o ramo “não” é um caso de teste que costuma faltar. O nó laranja — rollback e compensação — é o mais crítico e o menos testado: é onde o sistema decide se uma falha parcial deixa lixo no banco ou volta limpo.

Um caso de erro específico merece destaque à parte: o retry depois de falha parcial. Se o cliente reenvia a mesma requisição porque não recebeu resposta (timeout, conexão caiu), o servidor pode ter processado a primeira tentativa e não ter conseguido responder — o retry chega num sistema que já mudou de estado. Testar isso é perguntar: a operação é idempotente? Rodar pagar() duas vezes com o mesmo idempotency_key cobra o cliente uma vez ou duas? Esse é o edge case que junta duas linhas do checklist (“Erros de rede” + “Concorrência e falha parcial”) com o pensamento de state-transition: o segundo pagar() chega num pedido que já não está mais em Criado.

Conexão com property-based

Quando o espaço de casos é grande demais para enumerar à mão, dá para fazer a máquina gerar os edge cases por você — é o que faz o property-based testing, que cobro em 13 - Além do básico - property-based, snapshot, contract, smoke. As técnicas desta nota continuam valendo: elas dizem ao gerador onde olhar (fronteiras, classes, estados).

Como escolher a técnica certa

As quatro técnicas não competem — respondem perguntas diferentes sobre o mesmo sistema. A tabela abaixo é o resumo que eu uso pra decidir por onde começar diante de uma função nova (categorização segundo o ISTQB Foundation Level Syllabus, seção 4.2):

TécnicaPergunta que respondePega este tipo de bugNão pega
Equivalence partitioning”Que grupos de entrada o sistema trata igual?”Categoria inteira tratada errado (ex.: todo null derruba a função)Bug que só aparece na borda exata de uma classe
Boundary value analysis”O que acontece bem na fronteira?”Off-by-one, <= vs <, limite mal calculadoBug de combinação entre duas variáveis diferentes
Decision tables”O que acontece quando regras se combinam?”Regra de negócio esquecida numa combinação específicaComportamento que depende de estado anterior
State-transition”O que acontece dependendo de onde o sistema está?”Transição proibida aceita, sequência inválida ignoradaBug de valor de entrada isolado (sem estado)

A pergunta que cada técnica responde também indica quem costuma lembrar de aplicá-la: EP e BVA vêm naturalmente pra quem já pensa em tipos e limites; decision tables exigem alguém falando a língua do negócio (analista, PO, ou o próprio dev lendo a regra com atenção); state-transition exige enxergar o sistema como máquina de estados, o que nem todo mundo faz por hábito — é a técnica mais frequentemente esquecida das quatro.

Na prática, uma função real combina as quatro camadas: primeiro particione o domínio, depois vá às bordas de cada partição, depois combine as partições que interagem via tabela de decisão e, se houver estado envolvido, sobreponha state-transition. Cada técnica reduz o espaço de casos que a próxima precisa cobrir.

Note também a ordem de custo crescente: EP é a mais barata (poucos representantes, sem combinação), BVA soma pouco por cima (as mesmas partições, olhando as bordas), decision table já exige mapear regras de negócio explicitamente, e state-transition exige modelar o sistema inteiro como máquina de estados. Comece sempre pelas duas primeiras — elas sozinhas já capturam a maioria dos bugs de entrada isolada — e só suba para tabela/estado quando o domínio realmente tiver regras combinadas ou memória.

Exemplo trabalhado: bloqueio de conta por tentativas de login

Sobre este exemplo

Este é um exemplo ilustrativo e construído para amarrar as quatro técnicas — não é um caso extraído de um sistema real. Uso a mesma convenção do exemplo de cadastro de idade e da comissão de vendas acima: uma regra simples o bastante para caber numa nota, complexa o bastante para exercitar as quatro camadas juntas.

Nenhum dos casos usados nesta nota vem de um sistema em produção específico — todos são construções pedagógicas no mesmo espírito dos exemplos clássicos de Myers e do syllabus ISTQB, escolhidos por serem reconhecíveis (idade, comissão, pedido, login) sem depender de contexto de negócio que o leitor não tenha.

Regra: uma conta aceita até 5 tentativas de senha errada. Na 5ª tentativa errada, a conta trava por 15 minutos. Durante o bloqueio, qualquer tentativa — mesmo com a senha certa — é recusada.

1. Equivalence partitioning no parâmetro senha: classe “correta” e classe “incorreta” (qualquer senha errada é tratada igual, não importa quão errada).

2. Boundary value analysis no parâmetro failed_attempts: o limite é 5, então os casos que importam são 4 (ainda permite tentar), 5 (trava) e 6 (já deveria estar bloqueado antes de chegar aqui — indica bug se a tentativa 6 for processada normalmente).

3. Decision table para cruzar “senha correta?” com “conta bloqueada?”:

#Senha correta?Conta bloqueada?Resultado
1simnãologin aceito
2nãonãologin recusado, failed_attempts += 1
3simsimlogin recusado (bloqueio ignora senha correta)
4nãosimlogin recusado

A linha 3 é a que mais gente esquece de testar: senha certa não desbloqueia a conta antes da hora. Sem a tabela, é fácil testar só “senha errada + bloqueado” (linha 4) e achar que cobriu o cenário de bloqueio.

4. State-transition para o ciclo de vida da conta:

stateDiagram-v2
    [*] --> Ativa
    Ativa --> Ativa: senha_correta / zera contador
    Ativa --> Ativa: senha_errada [contador < 5] / incrementa
    Ativa --> Bloqueada: senha_errada [contador == 5]
    Bloqueada --> Bloqueada: qualquer_tentativa [dentro dos 15min]
    Bloqueada --> Ativa: tempo_expirado [15min]

Lead-in: o ciclo de vida da conta, com as condições de guarda entre colchetes controlando cada transição.

Leitura do diagrama: repare que Bloqueada --> Bloqueada é uma transição que consome qualquer tentativa sem mudar de estado — é exatamente a linha 3 da tabela de decisão, agora modelada como estado. E a transição Bloqueada --> Ativa só dispara por tempo, não por ação do usuário: é um edge case de state-transition que BVA sozinho nunca acharia, porque 15min não é fronteira de um parâmetro de entrada, é fronteira de tempo decorrido — categoria que está no checklist (“Timezone / horário de verão”) mas que aqui aparece como gatilho de transição.

As quatro técnicas, juntas, geram um conjunto pequeno e justificável: ~3 casos de EP, ~3 de BVA, 4 linhas de decision table e as transições do diagrama (incluindo as ausentes) — cobrindo o comportamento completo sem enumerar as combinações de failed_attempts (0 a 5) × senha (certa/errada) × bloqueada (sim/não) uma a uma.

Em uma linha

Nenhuma técnica sozinha teria achado o caso “senha certa não desbloqueia antes da hora” — ele mora exatamente na interseção entre decision table e state-transition, e é esse tipo de interseção que separa um catálogo de técnicas de um método de teste de verdade.

Armadilhas comuns

Cada técnica desta nota tem um jeito característico de dar falso senso de cobertura — parece que você testou o suficiente, mas o bug que passa é sempre da mesma família. As quatro abaixo são as que mais aparecem em code review, e não por coincidência: cada uma corresponde a uma das quatro técnicas cobertas acima, na mesma ordem em que apareceram.

A armadilha da partição de equivalência

Ela assume que o sistema de fato trata a classe inteira igual. Se houver um ramo escondido — digamos, uma regra especial para idade exatamente 65 (aposentadoria) dentro da faixa “válida” — a partição grossa não pega. Por isso você combina com as próximas técnicas e, eventualmente, espia a cobertura.

O erro clássico (state-transition)

Testar só as transições felizes (criado → pago → enviado → entregue) e esquecer as inválidas. O bug que pega produção é justamente o pedido que aceitou “despachar” sem ter sido pago — porque ninguém testou a transição proibida. Para cada estado, pergunte: quais eventos NÃO deveriam funcionar aqui?

O esquecido que pega em produção (caminho de erro)

Testar a exceção não é só “verificar que lança”. É verificar o que acontece depois: a transação reverteu? O e-mail de confirmação NÃO foi enviado? O contador NÃO incrementou? A falha do caminho feliz dá erro 500 e alguém percebe. A falha do caminho de erro dá dado corrompido silencioso — e ninguém percebe até a auditoria.

O exagero também é armadilha

Worst-case testing (5^n), robustness testing (7^n) e 2-switch coverage existem — mas aplicá-los por padrão, em toda função, é trocar um problema (poucos casos) por outro (suíte lenta demais pra rodar, ninguém entende por que cada caso existe). Regra prática: comece com EP + BVA simples + 0-switch em tudo; suba pra worst-case/1-switch só onde a combinação de variáveis tem motivo de negócio pra interagir — exatamente o critério usado no exemplo de bloqueio de login acima.

O padrão comum às quatro armadilhas: todas nascem de parar cedo demais. Testar a classe válida mas não as duas inválidas; testar as transições felizes mas não as proibidas; testar que a exceção foi lançada mas não o que sobrou depois; escalar pra 7^n casos achando que mais é sempre melhor. O antídoto é o mesmo em todos os quatro casos — perguntar explicitamente “o que eu não testei aqui, e por quê?” antes de considerar a suíte pronta.

Em entrevista

Senior interviewers don’t want a list of happy-path cases — they want to see your method for choosing cases. Open by naming the techniques: “I start black-box, deriving cases from the contract. I use equivalence partitioning to group inputs the system treats the same, then boundary value analysis on the edges of those partitions, because that’s where off-by-one bugs live.” For anything with combined business rules, mention decision tables; for anything stateful, mention state-transition testing and stress that you test the invalid transitions, not just the valid ones. Then run the edge-case checklist out loud — empty, null, boundaries, overflow, unicode, dates, concurrency — and frame it as “thinking like an adversary looking for the crack.” Finish strong with the line that signals seniority: “and I always test the error paths, not just the happy path — the rollback and compensation logic is where the expensive production bugs hide.” If they push on white-box, connect it to coverage: structure-derived cases catch the branches the spec forgot.

Vocabulário

PTEN
partição de equivalênciaequivalence partitioning
classe de equivalênciaequivalence class
análise de valor limiteboundary value analysis (BVA)
valor limite / fronteiraboundary value
erro de um a maisoff-by-one error
tabela de decisãodecision table
transição de estadostate transition
transição inválidainvalid transition
condição de guardaguard condition
caso extremoedge case / corner case
caminho felizhappy path
caminho de erroerror path / sad path / unhappy path
caixa-pretablack-box
caixa-brancawhite-box
estouro (de inteiro)overflow
valor representanterepresentative value
caso de teste negativonegative test case
cobertura por sequêncian-switch coverage
teste do pior casoworst-case testing

Fontes

O que vem a seguir

As quatro técnicas desta nota derivam casos de teste de uma descrição do comportamento esperado — partições, fronteiras, regras, estados. Isso é engenharia de teste na prática, mas a pergunta por trás (“como eu sei que cobri os casos que importam, e não só os que pensei primeiro?”) tem uma resposta mais formal do lado da matemática: 05 - Técnicas de prova mostra como se prova que uma propriedade vale para todos os elementos de um domínio — inclusive infinito — em vez de amostrar representantes. Equivalence partitioning é, no fundo, uma versão prática e barata da mesma ideia: agrupar por uma relação de equivalência para não precisar checar caso a caso.

E quando o espaço de casos é grande demais até para o worst-case testing enumerar à mão — a combinatória 7^n deste texto cresce rápido — a saída é deixar a máquina gerar as entradas por você. 07 - Fuzzing mostra a versão automatizada e orientada por cobertura dessa ideia: um motor de fuzzing explora o espaço de entradas mutando bytes e observando quais caminhos de código cada mutação alcança, achando os edge cases que o checklist desta nota não previu.

Os dois destinos não competem entre si nem com esta nota: prova formal dá a certeza matemática que nenhuma quantidade de casos de teste dá; fuzzing dá a escala que a enumeração manual não dá; as técnicas catalogadas aqui são o meio-termo prático que roda em qualquer PR, todo dia, sem exigir nem um teorema nem um motor de mutação.

Veja também