ChaosSQL

SDK

Go Developer Testing SDK (pkg/chaostest)

Como integrar o ChaosSQL diretamente em suítes de testes unitários do Go (*testing.T): referência completa da API fluente do builder chaostest.New(), asserções de invariantes e síntese automática de repro_test.go.

Introdução ao SDK (pkg/chaostest)

O ChaosSQL Go Developer Testing SDK (pkg/chaostest) foi desenvolvido para engenheiros de software que desejam incorporar testes de concorrência e invariantes diretamente nas suas suítes de testes unitários em Go (*testing.T). Ele permite testar camadas de persistência, repositórios e serviços de domínio diretamente via comando nativo go test ./..., com suporte completo a go test -race e sem a necessidade de instalar ou gerenciar binários externos.

API Fluente do Builder: chaostest.New(t)

O pacote expõe um padrão builder fluente com encadeamento de métodos para configurar o cenário de concorrência de forma intuitiva:

Método Parâmetros Descrição Técnica
New(t testing.TB) testing.TB Inicializa uma nova instância de teste atrelada ao ciclo de vida do teste atual.
WithDriver(driver) drivers.DatabaseDriver Define a instância do driver. Se omitido, provisiona automaticamente um SQLite in-memory isolado.
WithSchema(sql) string Define os comandos SQL DDL para criação de tabelas e índices.
WithSeed(sql) string Define os comandos SQL DML para carga inicial de dados antes do teste.
WithInvariant(name, query, expr) string, string, string Registra uma regra de negócio que deve ser preservada ao final do teste.
WithJitter(minMs, maxMs) int, int Configura a faixa de jitter em milissegundos injetada entre os passos de cada transação (padrão: 1ms a 5ms).
AddOperation(name, steps...) string, ...string Registra uma transação concorrente. Suporta sintaxe de captura -> var e expressões {var - 100}.
AddOperationWithParams(name, params, steps...) string, map[string]string, ...string Registra uma operação com parâmetros gerados dinamicamente por transação.

Asserções de Teste: AssertNoAnomalies vs AssertAnomalyDetected

1. AssertNoAnomalies(ctx, workers, iterations, seed)

Asserção positiva: valida que a implementação do serviço ou repositório não apresenta violação de invariante nem anomalia sob a semente e o agendamento configurados. É evidência para esse agendamento, não prova para todos. Se uma anomalia for detectada ou uma invariante for violada, o método invoca t.Fatalf reportando:

  • Nome e valor da invariante que falhou.
  • Classificação formal da anomalia no catálogo Adya (ex: P4_LOST_UPDATE).
  • Métricas do Delta-Debugging (ddmin): redução de N operações originais para o caso minimal.
  • Diagrama de Sequência Mermaid formatado pronto para copiar.

2. AssertAnomalyDetected(ctx, workers, iterations, seed, expectedAnomaly...)

Asserção negativa: utilizada em testes de regressão para provar empiricamente que um código vulnerável ou um nível de isolamento insuficiente realmente dispara a anomalia prevista. Use para manter o teste de regressão honesto: se um refactor deixar de reproduzir o bug, o teste falha.

Exemplo Completo: Testando e Mitigando Lost Update

O exemplo abaixo demonstra como escrever um teste unitário em Go que comprova a eficácia de uma mitigação concorrente (migração de Read-Modify-Write vulnerável para um UPDATE atômico com guarda):

package bank_test

import (
    "context"
    "testing"

    "github.com/bregaldahq/chaossql/internal/drivers"
    "github.com/bregaldahq/chaossql/pkg/chaostest"
)

// TestAccountWithdrawal_AtomicMitigation valida que saques concorrentes
// usando update atômico impedem a ocorrência de Lost Update (P4).
func TestAccountWithdrawal_AtomicMitigation(t *testing.T) {
    ctx := context.Background()

    // 1. Instanciar driver de banco de dados (SQLite puro em memória)
    driver := drivers.NewSQLiteDriver("file:bank_test?mode=memory&cache=shared")
    defer driver.Close()

    // 2. Construir o fuzzer de concorrência com o SDK fluente
    tester := chaostest.New(t).
        WithDriver(driver).
        WithSchema(`
            CREATE TABLE accounts (
                id INT PRIMARY KEY,
                balance INT NOT NULL
            );
            CREATE TABLE ledger (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                amount INT NOT NULL
            );
        `).
        WithSeed("INSERT INTO accounts (id, balance) VALUES (1, 1000);").
        WithInvariant(
            "ledger_balance_consistency",
            "SELECT (SELECT balance FROM accounts WHERE id = 1) AS actual, " +
            "(1000 - COALESCE(SUM(amount), 0)) AS expected FROM ledger;",
            "actual == expected",
        ).
        WithJitter(1, 4).
        // Transação mitigada: UPDATE atômico com verificação de saldo suficiente
        AddOperation("atomic_withdrawal",
            "UPDATE accounts SET balance = balance - 100 WHERE id = 1 AND balance >= 100;",
            "INSERT INTO ledger (amount) VALUES (100);",
        )

    // 3. Executar o fuzzer com 8 workers simultâneos e 40 transações
    // Deve passar com 100% de sucesso sem violação de invariantes
    tester.AssertNoAnomalies(ctx, 8, 40, 42)
}

Síntese Automática de repro_test.go pelo ddmin

Quando um teste executado via SDK falha com AssertNoAnomalies, o algoritmo Delta-Debugging (ddmin) não apenas isola o subconjunto de 2 operações mínimas, mas também sintetiza no log do teste o código fonte completo em Go de um arquivo repro_test.go independente.

Esse código gerado pode ser colado em um arquivo local para depuração com breakpoints no Delve, permitindo que a equipe reproduza o defeito sem executar o fuzzer.

Determinismo Estrito em Testes de CI: Ao configurar testes de regressão no Go, utilize sempre uma semente estática constante (ex: seed: 42). A mesma semente produz o mesmo agendamento em todo runner de CI, então uma falha se repete em vez de oscilar (flakiness).