Cenários: concorrência, tempo e rede
Provar que o mutex segura, que o cache vence amanhã, e o que o cliente faz com um 503.
Os matchers respondem "o valor é o esperado?". Estas quatro ferramentas respondem uma pergunta anterior: em que mundo o código está rodando? Elas montam o cenário — e não comparam nada.
Concorrência: a corrida que o `check` só avisa#
A linguagem não sincroniza sozinha, e isso está documentado: duas threads escrevendo no mesmo nome perdem atualizações, em silêncio. O check avisa sobre o padrão (escrita-concorrente) — mas avisar não é provar, e um teste que roda a ação uma vez por thread não detecta nada, porque a janela é estreita.
action test_o_contador_perde_sem_mutex():
contador := spawn Contador()
r := Crucible.corrida(lambda => contador.somar(),
threads := 4, voltas := 5000,
leitor := lambda => contador.valor())
expect(r.perdeu()).to_be_true()
out $"perdeu {r.perdidas()} de {r.esperado}"
action test_com_mutex_nao_perde():
contador := spawn ContadorProtegido()
r := Crucible.corrida(lambda => contador.somar(),
threads := 4, voltas := 5000,
leitor := lambda => contador.valor())
expect(r.perdeu()).to_be_false()| O que diz | |
|---|---|
r.perdeu() | yes quando o total não bate |
r.perdidas() | quantas atualizações sumiram |
r.erros | o que estourou dentro da thread — sem isto, morreria calado |
r.para_vault() | tudo, para um relatório |
Determinismo: o mesmo dado dá o mesmo resultado?#
r := Crucible.determinismo(lambda => montar_relatorio(vendas), vezes := 5)
expect(r["estavel"]).to_be_true()É a propriedade que um relatório precisa ter e que quase nada tem: um id() na chave de um cache, uma ordem de vault, um random sem semente — os três passam no teste que roda uma vez.
A comparação é por foto estrutural, e não por referência: duas listas iguais são objetos diferentes, e comparar referência diria "instável" para código perfeitamente determinístico.
O relógio que anda#
freeze_time congela; o relógio anda. A diferença importa para testar o que depende de intervalo — um cache com validade, um recuo, um prazo — porque congelado eles nunca vencem, e com o relógio de verdade o teste precisa dormir.
action test_o_cache_vence_em_trinta_minutos():
Crucible.com_relogio(lambda r => conferir_validade(r),
"2026-09-20 10:00:00")
action conferir_validade(r):
cache.guardar("cotacao", 5.42)
r.avancar(minutos := 29)
expect(cache.obter("cotacao")).to_be(5.42)
r.avancar(minutos := 2)
expect(cache.obter("cotacao")).to_be_void()Um HTTP que você controla#
Um dublê substitui o cliente e prova que o código chamou um método. O servidor falso sobe um socket de verdade e prova que o código fala HTTP direito — cabeçalho, corpo, status — e o que ele faz com um 503. São perguntas diferentes, e a segunda é a que quebra em produção.
action test_o_cliente_repete_duas_vezes_e_desiste():
s := Crucible.servidor_falso()
s.falhar("/precos", 503, vezes := 2)
s.responder("/precos", {"dolar": 5.42})
resposta := meu_cliente.buscar(s.url("/precos"))
expect(resposta["dolar"]).to_be(5.42)
expect(s.quantos("/precos")).to_be(3)
s.parar()O vezes é o que torna testável o "falha duas vezes e na terceira funciona" — o comportamento que um cliente com recuo promete e quase nunca tem teste.
| Programar | Perguntar |
|---|---|
s.responder(rota, corpo, status) | s.pedidos(rota) — tudo o que chegou |
s.falhar(rota, status, vezes) | s.ultimo(rota) — o último, com corpo e cabeçalhos |
s.demorar(segundos) | s.quantos(rota) |
s.url(caminho) | s.limpar() · s.parar() |
Uma rota que ninguém programou responde 404, e não um corpo vazio: um 200 sem conteúdo faria o teste falhar num ponto distante, dizendo que o dado veio errado.