Skip to content

O catálogo de tipos

A pirâmide organiza os testes por quanto do sistema exercitam de uma vez. Ela não diz nada, porém, sobre que tipo de checagem cada teste faz, e é aí que mora um leque de instrumentos que resolvem problemas que o teste comum não resolve bem.

O teste comum é baseado em exemplos: quem escreve escolhe algumas entradas, calcula à mão o resultado esperado e afirma a igualdade. É direto e insubstituível, mas tem um ponto cego estrutural, ele só cobre os casos que a pessoa pensou em cobrir. O bug mora justamente no caso que não ocorreu a ninguém: a lista vazia, o número negativo, o caractere Unicode estranho, a data no fuso errado. Boa parte do catálogo a seguir existe para atacar esse ponto cego por ângulos diferentes. O restante existe para medir dimensões que “está certo?” não captura: rápido o bastante, seguro o bastante, acessível o bastante.

Vale fixar, antes de entrar no leque, a divisão mais grossa de todas. O teste funcional pergunta se o sistema produz o resultado certo. O teste não funcional pergunta o quão bem ele se comporta ao produzir esse resultado: sob carga, sob ataque, para quem usa leitor de tela. São eixos ortogonais, e um sistema pode passar em todo teste funcional e ainda assim falhar no não funcional, respondendo certo, porém tarde demais ou de forma insegura.

O primeiro salto sobre o teste de exemplo é parar de escolher as entradas e passar a gerá-las. Em vez de afirmar “para a entrada 3, a saída é 9”, afirma-se uma propriedade que deve valer para toda entrada, e uma ferramenta bombardeia a função com centenas de casos aleatórios procurando um que a viole. Quando acha, ela encolhe o contra-exemplo até a forma mínima que ainda quebra, entregando não um caso obscuro de 4 mil elementos, mas o menor culpado possível. Essa é a ideia do teste baseado em propriedades, nascido no QuickCheck de Koen Claessen e John Hughes para Haskell, em 2000, e hoje presente em quase toda linguagem. O ganho é encontrar o caso de borda que ninguém imaginou; o custo é que achar uma boa propriedade (“inverter a lista duas vezes devolve a lista”) exige mais raciocínio do que escrever um exemplo, e nem todo comportamento se deixa expressar como propriedade. O casamento entre essa técnica e o estilo funcional é forte o bastante para merecer seu próprio tratamento.

O fuzzing é o primo dessa ideia voltado à robustez e à segurança. Em vez de gerar entradas bem formadas para checar uma propriedade lógica, ele despeja entradas deformadas e aleatórias procurando fazer o programa quebrar de verdade: estourar memória, travar, entrar em loop. As ferramentas modernas são guiadas por cobertura, isto é, observam quais caminhos do código cada entrada aciona e mutam as entradas para alcançar caminhos novos, o que as torna surpreendentemente boas em achar a sequência exata de bytes que dispara uma falha. O AFL (American Fuzzy Lop), criado por Michal Zalewski e liberado pelo Google, popularizou a abordagem, e o serviço OSS-Fuzz roda fuzzers continuamente contra centenas de projetos abertos. O fuzzing brilha em código que consome dados não confiáveis (parsers, decodificadores, formatos binários) e é onde o retorno por real gasto é mais alto; fora desse território, custa mais montar o arnês do que se ganha.

Uma suíte verde diz que os testes passaram, não que eles prestam. A pergunta “meus testes de fato pegariam um bug?” tem uma resposta mecânica, e ela se chama teste de mutação. A ferramenta semeia defeitos artificiais no código de produção, um por vez (troca um + por um -, um < por um <=, remove uma linha), gerando “mutantes”, e roda a suíte contra cada um. Se algum teste falha, o mutante foi morto, o que é bom: significa que a suíte detectaria aquele defeito. Se todos passam, o mutante sobreviveu, e a suíte tem um buraco ali. A proporção de mutantes mortos é o escore de mutação. As ferramentas canônicas são o PIT, em Java, e o Stryker, em JavaScript.

O valor do teste de mutação fica claro quando se compara com a cobertura de código, a métrica que a maioria conhece. Cobertura mede apenas quais linhas foram executadas pelos testes, e um teste sem nenhuma asserção executa a linha, some na cobertura e não pega bug algum. O teste de mutação mede a outra coisa, a capacidade de detectar o defeito, e por isso o PIT se descreve como o padrão-ouro contra o qual as demais coberturas se medem. Convém ler esse “padrão-ouro” como o que é, o argumento de quem faz a ferramenta: a visão mais equilibrada é que mutação e cobertura são complementares, que cobertura só serve como indicador negativo (cobertura baixa é sinal ruim, cobertura alta não garante nada) e que mutantes equivalentes, que mudam o código sem mudar o comportamento, tornam um escore de 100% inalcançável na prática. Ainda assim, é a técnica que expõe sem dó o teste que ficou verde sem afirmar nada, e essa vai ser a exata patologia dos testes gerados por IA.

Há uma família de testes cujo oráculo não é uma resposta calculada à mão, e sim o comportamento atual do próprio sistema. Eles não afirmam “o certo é X”; afirmam “continua igual à última vez que aprovamos”. A economia é enorme, porque dispensa saber a resposta certa, e o risco é o reverso da mesma moeda, porque fixam o que existe, bug e tudo.

O teste de snapshot, popularizado pelo Jest, captura a saída serializada de algo (o HTML que um componente renderiza, uma estrutura de dados) num arquivo, e a cada execução compara a saída nova com a guardada, falhando em qualquer diferença. Entrega detecção de regressão a custo quase zero para saídas que mudam pouco. O problema, bem catalogado por Artem Sapegin, é que o snapshot apodrece: quando quebra, a reação reflexa é regenerá-lo com um comando (jest -u) sem olhar, o que transforma o teste num carimbo vazio; uma mudança pequena num componente compartilhado faz centenas de snapshots quebrarem de uma vez; e, no limite, o teste afirma apenas que o HTML é idêntico, que quase nunca é a propriedade que de fato importa. Snapshot é útil na dose certa e traiçoeiro quando vira o hábito padrão.

A versão dessa ideia voltada a código legado é o teste de caracterização, termo cunhado por Michael Feathers em Working Effectively with Legacy Code, de 2004. Diante de um código sem testes que é preciso alterar, escreve-se um teste que apenas fixa o comportamento observável atual, seja ele qual for, para servir de rede antes do refactor: se a mudança alterar algo sem querer, o teste de caracterização acusa. É o mesmo princípio do golden master, que guarda uma saída de referência e compara as futuras contra ela, e do teste de aprovação de Llewellyn Falco, que na falha abre uma ferramenta de diff para um humano inspecionar e, se a nova saída estiver correta, promovê-la a aprovada. Todos compram uma rede para código que não se entende inteiro; todos correm o risco de canonizar um defeito como se fosse a especificação.

Quando o sistema é dividido em serviços que se chamam pela rede, surge um risco que nenhum teste de unidade pega e que os testes ponta a ponta pegam caro demais: o consumidor e o provedor de uma API divergirem sobre o formato das mensagens. O teste de contrato ataca exatamente essa fronteira. Na variante dirigida pelo consumidor, que a ferramenta Pact popularizou, o consumidor escreve testes que expressam o que ele espera da API, esses testes são serializados num arquivo de contrato, e o provedor depois roda uma verificação para garantir que seu comportamento real satisfaz aquele contrato. A vantagem é testar só as partes da API que algum consumidor de fato usa, e fazê-lo verificando cada lado isoladamente, sem precisar subir todos os serviços juntos. É o instrumento que realiza a promessa do favo de mel, testar a interação sem depender dos outros sistemas estarem no ar.

Alguns nomes de teste não descrevem uma técnica, e sim o momento e a intenção com que se roda um conjunto de testes. São ortogonais a tudo acima: um teste de fumaça pode ser de unidade ou ponta a ponta, o que o define é o papel.

  • Teste de fumaça: uma checagem rápida e rasa de que o build não está obviamente quebrado, o suficiente para decidir se vale a pena rodar o resto. Responde “isto de pé?”, não “isto correto?”.
  • Teste de sanidade: uma checagem estreita e específica de que uma correção pontual fez o que devia, antes de investir na bateria completa. Onde a fumaça é rasa e larga, a sanidade é estreita e funda.
  • Teste de regressão: a bateria abrangente que re-verifica o que já funcionava, para garantir que a mudança nova não quebrou nada antigo. É o papel que a suíte inteira cumpre ao longo do tempo.
  • Teste de aceitação: a validação de que o sistema faz o que o negócio pediu, na linguagem do negócio. É onde entram o BDD e o formato dado-quando-então, que Dan North formalizou por volta de 2006 e que ferramentas como o Cucumber tornam executável, aproximando o teste de uma especificação que o cliente consegue ler.

Fora da pergunta “está certo?” vive o eixo do “quão bem?”, e ele tem suas próprias famílias de teste, cada uma com ferramental próprio.

  • Desempenho: teste de carga mede o comportamento sob o tráfego esperado; teste de estresse empurra além da capacidade para ver onde e como o sistema quebra; teste de resistência (soak) mantém uma carga moderada por horas para revelar vazamentos de memória e degradação lenta. As ferramentas comuns são k6, JMeter, Gatling e Locust.
  • Segurança: análise estática de segurança (SAST) vasculha o código-fonte em busca de padrões perigosos sem executá-lo; análise dinâmica (DAST) ataca a aplicação rodando, como um invasor faria, para achar injeção, XSS e falhas de autenticação. O OWASP ZAP é o DAST aberto de referência.
  • Acessibilidade: verifica a conformidade com as diretrizes WCAG, e o axe-core, da Deque, é o motor automatizado mais usado. Vale a ressalva de que ferramentas automáticas cobrem só uma fração dos critérios (da ordem da metade), e o resto exige julgamento humano, então a checagem automatizada é o piso, não o teto.

A régua de confiança sobre custo continua valendo em todo o catálogo, mudando apenas o que está na balança. O teste baseado em propriedades compra abrangência ao preço de exigir uma boa propriedade; o de mutação compra honestidade da suíte ao preço de rodar tudo muitas vezes; o snapshot compra regressão barata ao preço de apodrecer; o de contrato compra a fronteira ao preço de manter contratos em dia. Escolher o instrumento é escolher qual dessas trocas o seu risco justifica.

Técnicas: teste baseado em propriedades, fuzzing, teste de mutação, snapshot, golden master, teste de caracterização, teste de aprovação, teste de contrato Ferramentas: QuickCheck, AFL, OSS-Fuzz, PIT, Stryker, Jest, Pact, Cucumber, k6, JMeter, Gatling, Locust, OWASP ZAP, axe-core Papéis: teste de fumaça, teste de sanidade, teste de regressão, teste de aceitação, BDD, dado-quando-então Não funcional: carga, estresse, resistência, SAST, DAST, acessibilidade, WCAG Métricas: escore de mutação, cobertura de código Pessoas: Koen Claessen, John Hughes, Michal Zalewski, Michael Feathers, Llewellyn Falco, Dan North, Artem Sapegin