Pular para o conteúdo

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.

dataforge
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.erroso 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?#

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

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

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

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