Testing guide
Running tests
Rust: all crates:
cargo test --workspace
This runs unit tests, doc tests, and integration tests across undolog-engine, undolog-store, undolog-types, and undolog-saga.
To run a single crate's tests:
cargo test -p undolog-engine
To run a specific test by name:
cargo test --workspace -- test_name_prefix
Go: proxy service:
cd services/undolog-proxy
go test ./... -v -count=1
The -count=1 flag disables test caching. Use -v for verbose output, -run to filter by test name.
Python. SDK:
cd sdks/undolog-py
pytest -v
Uses pytest-asyncio for async test support. Tests requiring a running engine use the engine fixture defined in conftest.py.
Test database
Integration tests that exercise the PostgreSQL store require a test database. Set the TEST_DATABASE_URL environment variable:
export TEST_DATABASE_URL=postgres://undolog:undolog@localhost:5432/undolog_test
The test harness creates the schema on first use and truncates tables between test runs. If you run tests without this variable, tests that require a database are skipped.
Writing tests
Naming conventions
| Language | Convention | Example |
|---|---|---|
| Rust | #[cfg(test)] module, fn test_<description>() |
fn test_compensation_registers_before_execute() |
| Go | func Test<Name>(t *testing.T) |
func TestCompensationRegistersBeforeExecute(t *testing.T) |
| Python | def test_<description>() |
def test_compensation_registers_before_execute() |
Asserting error types
Rust:
let err = store.register_effect(&effect).await.unwrap_err();
assert!(matches!(err, StoreError::DuplicateSignature { .. }));
Go:
err := store.RegisterEffect(ctx, &effect)
var se *StoreError
if errors.As(err, &se) {
assert.Equal(t, ErrDuplicateSignature, se.Code)
}
Python:
with pytest.raises(DuplicateSignatureError):
await store.register_effect(effect)
Mocking the gRPC/proxy boundary
The Go proxy communicates with the Rust engine over gRPC. In Go tests, use a gRPC test server:
func TestProxyRegistersEffect(t *testing.T) {
srv := startTestEngineServer(t)
defer srv.Stop()
client := connectToEngine(t, srv.Addr())
// test against client
}
Define helper functions in a shared testutil package to avoid duplicating test server setup across files.
Testing compensations
Compensation handlers are HTTP endpoints. To test that a compensation is called correctly:
- Start a local HTTP test server that records incoming requests.
- Register the compensation through the engine or proxy.
- Trigger the compensation path.
- Assert that the test server received the expected request.
Rust example, using wiremock or a simple actix_web / axum test server:
let mock_server = MockServer::start().await;
let uri = mock_server.uri();
// Register compensation pointing at mock_server
engine.register_compensation(effect_id, &uri).await?;
// Trigger compensation
engine.compensate(effect_id).await?;
// Verify mock was called
mock_server.verify().await;
Property-based testing
Use proptest in Rust for property-based tests that validate invariants. Property tests are preferred over example-based tests for state machine logic.
proptest! {
#[test]
fn valid_state_transitions_never_produce_invalid_state(
start in any::<EffectState>(),
transition in any::<StateTransition>(),
) {
let result = apply_transition(start, transition);
if let Ok(next) = result {
prop_assert!(next.is_valid());
}
}
}
Key invariants to property-test:
- Every valid state transition produces a valid next state
- Compensation is always registered before the action executes
- After crash recovery, the undo stack contains no
runningentries without a corresponding compensation registration - Advisory lock keys are deterministic across languages for the same input
Performance tests
Performance benchmarks live in benches/ directories within each Rust crate. Use criterion for microbenchmarks.
Run benchmarks:
cargo bench --workspace
Benchmark guidelines:
- Test at realistic data sizes (not just the happy path with one effect)
- Test concurrent access patterns with multiple goroutines/tasks
- Test crash recovery timing with a realistic undo stack size
- Document the hardware and database configuration in the benchmark output