Qualquer engenheiro desconfia de um build vermelho. O artefacto perigoso é o verde, porque o verde transporta uma afirmação implícita: isto foi medido, e aguentou. Essa afirmação tem dois modos de falha e só um é famoso. Um teste pode estar errado sobre o sistema. Um teste também pode estar certo sobre o nada: apontado a um artefacto que ninguém publica, a um ficheiro que ninguém carrega, a uma promessa que ninguém corre. Passa para sempre, e não protege coisa nenhuma.
Passámos uma época a coleccionar espécimes do segundo tipo, nos nossos próprios repositórios, que é onde uma colecção destas deve começar. Cinco deles, com o código, e o hábito que teria apanhado cada um mais cedo.
Espécime um: a suite que verificava um fantasma
A migrar um site entre dois renderizadores, a nossa suite lia o directório de saída publicado e verificava centenas de propriedades contra ele:
SWA = ROOT / "swa" # the directory the deploy uploads
def test_every_route_declares_its_canonical():
for page in SWA.rglob("index.html"):
assert '<link rel="canonical"' in page.read_text()Verde durante semanas. Quatrocentas e vinte rotas verificadas. O problema: o swa/ ainda era escrito pelo renderizador antigo. O novo, que era o objectivo inteiro do projecto, escrevia para .build/astro/, e teste nenhum abria essa pasta. Estávamos a verificar o artefacto que íamos substituir.
A cura não foram mais asserções. Foi uma pergunta feita à suite inteira, que bytes é que isto abre, realmente?, respondida por fazer a suite compor primeiro o artefacto publicável e acrescentar um teste cujo único trabalho é falhar se o directório publicado não for essa composição:
def test_the_published_artefact_is_the_composition():
# `data-page-owner` is written by the new renderer and nothing else.
# If no published page carries it, swa/ is still the old site.
owned = [p for p in SWA.rglob("*.html") if "data-page-owner" in p.read_text()]
assert owned, "the published artefact is the old renderer; the composition never shipped"Aborrecido, estrutural, e o teste mais valioso do repositório, porque valida a única relação que todos os outros assumem em silêncio.
Espécime dois: o portão que lia texto, não comportamento
O nosso colector de evidência traz uma promessa dura: só leitura. É imposta com o parse de cada ficheiro PowerShell e uma caminhada pela árvore de sintaxe à procura de verbos mutantes:
def test_no_powershell_file_has_a_write_path():
for f in COLLECTOR.rglob("*.ps1"):
ast = parse_powershell(f.read_text())
verbs = [c.command_name for c in ast.find_all(CommandAst)]
assert not [v for v in verbs if v.split("-")[0] in MUTATING], fUm bom portão. Também um portão sobre texto. Uma versão ensinou-nos a diferença: um Import-Module -Force aninhado descarregava um helper partilhado da sessão de quem chamava, e o colector não conseguia correr de todo, em tenant nenhum e em modo nenhum:
# The defect. -Force on a nested import re-imports the module fresh,
# evicting the already-loaded copy from the caller's session.
Import-Module (Join-Path $PSScriptRoot 'Evidence.psm1') -Force # Initialize-Evidence now gone upstreamTodos os portões de texto ficaram verdes, porque o texto estava bem. O programa que o texto descrevia estava partido. A correcção foi uma segunda guarda que carrega os módulos exactamente como o colector os carrega e pergunta à sessão viva o que sobreviveu:
Describe 'the modules load in the collector''s own order' {
It 'keeps Initialize-Evidence reachable after the siblings import' {
Import-Module ./modules/Evidence.psm1
Import-Module ./modules/Sharing.psm1 # must not evict the above
Get-Command Initialize-Evidence -ErrorAction Stop | Should -Not -BeNullOrEmpty
}
}A lição generaliza para lá do PowerShell: para cada propriedade que impõe estaticamente, pergunte o que acontece se o código passar na verificação e mesmo assim não fizer a coisa. Se nada desse por isso, o seu portão prova analisabilidade, não comportamento, e devia dizê-lo em voz alta em vez de pedir emprestada a autoridade da afirmação mais forte.
Espécime três: a tabela publicada que nunca corria
O nosso motor documenta como raciocina sobre evidência incompleta: uma tabela de operadores e limites, que lado consegue decidir, que lado nunca pode:
operator lower bound proves what it cannot prove
> (min) pass fail (needs an upper bound)
not-contains fail on known part pass (needs the whole set)
== nothing decides only when single and fully observedEstava no documento de arquitectura, citada, polida, e, como um relatório de cobertura revelou, mal era executada por teste algum. Verdadeira no dia em que foi escrita e sem guarda para sempre; qualquer refactor podia inverter uma linha em silêncio e o documento continuaria a prometer o comportamento antigo com cara séria. Por isso a tabela passou a executável, um caso por célula, gerado da mesma fonte que o documento apresenta:
@pytest.mark.parametrize("operator,bound,expected", CASES_FROM_THE_TABLE)
def test_each_cell_of_the_published_table(operator, bound, expected):
assert decide(operator, bound) is expected
def test_a_bound_of_none_decides_nothing():
assert decide(">", bound=None) is Outcome.UNKNOWNUma garantia publicada ou corre no CI ou é uma esperança com tipografia.
Espécime quatro: a fixture que lisonjeava o teste
O mais silencioso. Um teste avalia uma regra e espera unknown; passa:
def test_denied_sharing_run_is_all_unknown():
doc = load("coverage-denied-sharing.json")
assert doc.counts.unknown == 13 # ← where does 13 come from?A fixture foi gerada quando um defeito de selecção fazia o motor correr todas as regras em vez das duas do perfil. O 13 foi copiado desse output partido. Não era uma especificação; era um fóssil de um defeito, mantido vivo por um directório de fixtures que nada refrescava. Só o apanhámos quando as fixtures foram regeneradas a partir do motor actual e o fóssil discordou da realidade: o perfil selecciona duas regras, portanto a contagem honesta era três, não treze.
O hábito que o apanha mais cedo é um comentário:
# Three, because `--profile sharing` selects three rules since SPO-SHARE-005.
# It read thirteen for as long as a bare profile name failed open to every
# rule on disk, and this fixture was generated then.
assert doc.counts.unknown == 3Um número fixado que não se consegue citar é um número copiado do que o sistema fez no dia em que o teste foi escrito, e o sistema nesse dia podia estar errado.
Espécime cinco: os testes que provavam as regras e nunca a montagem
O maior, e o mais perto de casa. Sessenta e cinco testes, todos verdes, cobriam uma API regra a regra: validação, limites de ritmo, o tecto da moderação, a forma de um token. Nenhum deles abria o socket que um pedido abre. Cada teste exercitava um helper puro; nenhum conduzia o handler HTTP que junta esses helpers numa resposta. As regras estavam provadas. A montagem entre elas era assumida.
Por trás desse verde estava um mecanismo que não podia funcionar em produção. Uma edição da newsletter regista quem já a recebeu, para que uma repetição envie só a quem faltou. O marcador vai para uma tabela chamada newssent:
// The whole idempotency of a send rests on this row, and nothing creates the
// table it lives in.
await marker().upsertEntity(
{ partitionKey: editionId, rowKey: key(subscriber.email) },
"Replace",
);A produção não tinha tabela newssent. O primeiro envio mandava a edição, falhava a escrever o marcador numa tabela que não existia, e reportava o destinatário como falha. Pior, a guarda que lê o marcador trata uma tabela em falta como "ainda não enviado", por isso cada repetição mandava a lista inteira outra vez. A idempotência estava morta à nascença, e cada teste ficava verde, porque o duplo em memória que cada teste usava respondia ao createTable com um encolher de ombros e ao upsert com sucesso. Um falso não consegue não existir.
O que o mediu não foi mais uma asserção. Foi o mesmo handler, conduzido contra um Table Storage real a correr localmente, com a linha persistida lida de volta:
// Real handler, real Table Storage, real OData. Only a table that can truly be
// absent exposes the missing create; only a real query proves the filter.
const res = await handler("newsletter-send")(admin(edition), ctx());
const written = await readAll(sent); // read the state back, do not trust the return
assert.equal(written.length, res.sent); // "sent" must imply "recorded"A correcção foi uma linha que os outros armazéns já tinham, um createTable defensivo antes do ciclo. O defeito não é o ponto. O ponto é que uma suite pode provar todas as regras que um sistema tem e nunca provar que o sistema corre, e a falha fica invisível até um teste abrir o mesmo socket, e o mesmo armazenamento, que a produção vai abrir.
Vale nomear aqui uma garantia, porque nomeá-la é metade do trabalho. A entrega de uma mensagem confirmada à caixa, e de uma edição da newsletter, é de entrega no mínimo uma vez (at-least-once): um crash entre o envio e o seu registo repete o envio na repetição, nunca o deixa cair. É uma escolha. Um duplicado é um incómodo; uma mensagem perdida é um cliente. O teste não afirma "exactamente uma vez", que seria mentira; afirma que, depois de um crash em cada ponto, o trabalho continua recuperável, e nada é saltado em silêncio.
O verde é uma afirmação, não um estado
Cada teste a passar afirma uma relação entre três coisas: o código, o artefacto sob teste, e o mundo que o artefacto vai encontrar. Cada espécime partiu uma perna diferente. A suite media o artefacto errado. O portão media texto em vez de comportamento. O documento prometia o que nada executava. A fixture congelou um mundo partido e chamou-lhe esperado. A última suite provava todas as regras e nunca as corria juntas.
A cura é uma pergunta com cinco disfarces, e vale a pena fazê-la hoje à sua suite mais verde: se isto deixasse de ser verdade, alguma coisa aqui ficaria vermelha? Pegue no seu painel mais tranquilizador, no seu selo de CI mais estável, e siga uma luz verde até aos bytes que ela abre. Se o rasto acabar num artefacto que ninguém publica, numa promessa que ninguém corre ou num número sem fonte, a luz não está a medir o sistema.
Está a decorá-lo.