Skip to main content

Testing

Angzarr uses a three-level testing strategy. Each level has distinct scope and infrastructure requirements.


Three Levels of Testing


Unit Tests

No external dependencies. Tests interact only with the system under test — no I/O, no concurrency, no infrastructure.

The guard/validate/compute pattern makes business logic 100% unit testable:

# test_player.py
from player.handlers import guard_registered, validate_deposit, compute_deposit
from player.state import PlayerState
from proto.player_pb2 import DepositFunds

def test_guard_rejects_unregistered_player():
state = PlayerState(registered=False)

with pytest.raises(CommandRejectedError):
guard_registered(state)

def test_validate_rejects_non_positive_amount():
cmd = DepositFunds(amount=0)

with pytest.raises(CommandRejectedError):
validate_deposit(cmd)

def test_compute_deposit_increases_bankroll():
state = PlayerState(registered=True, bankroll=1000)
cmd = DepositFunds(amount=500)

event = compute_deposit(cmd, state)

assert event.new_bankroll == 1500

Integration Tests

Test Angzarr framework internals using synthetic aggregates. Prove the plumbing works.

What they cover:

  • Event persistence and sequence numbering
  • gRPC transport
  • Event bus pub/sub delivery
  • Saga activation and routing
  • Snapshot/recovery

Location: tests/standalone_integration/

Uses testcontainers for real databases:

#[tokio::test]
async fn test_event_persistence() {
let (_container, url) = start_postgres().await;
let pool = connect_and_migrate(&url).await;
let store = PostgresEventStore::new(pool);

// Test with synthetic aggregate
let events = store.add_events(&event_book).await.unwrap();
assert_eq!(events.len(), 1);
}

Acceptance Tests (Gherkin)

Test business behavior through the full stack. Written in Gherkin, describing what the system does from a business perspective.

Location: examples/*/tests/

Shared Feature Files

The same Gherkin scenarios validate all language implementations:

examples/
├── features/ # Shared Gherkin (canonical source)
│ ├── player.feature
│ ├── table.feature
│ ├── hand.feature
│ └── compensation.feature
├── python/features/ # Symlinks to ../features/
├── go/features/ # Symlinks to ../features/
├── rust/tests/features/ # Symlinks
├── java/tests/.../features/ # Symlinks
├── csharp/Tests/Features/ # Symlinks
└── cpp/tests/features/ # Symlinks

Example Feature: Player Reserve Funds

@player @aggregate
Feature: Player Aggregate
The Player aggregate manages bankroll and fund reservations.

@funds @reservation
Scenario: Reserve funds for table buy-in
Given a registered player "Alice" with bankroll 1000
When Alice reserves 500 for table "Main-1"
Then Alice's available balance is 500
And Alice's reserved balance is 500
And FundsReserved is emitted with:
| amount | 500 |
| table_id | Main-1 |

@funds @compensation
Scenario: Release funds when table join fails
Given Alice has reserved 500 for table "Main-1"
And JoinTable was rejected with reason "table_full"
When Alice receives the Notification
Then FundsReleased is emitted with amount 500
And Alice's available balance is restored to 1000

Running Tests by Language

# Python uses behave
cd examples/python
behave features/

# Run specific tags
behave features/ --tags=@player
behave features/ --tags=@compensation

Testcontainers

Storage backend tests use testcontainers to provision real databases:

async fn start_postgres() -> (ContainerAsync<GenericImage>, String) {
let image = GenericImage::new("postgres", "16")
.with_exposed_port(5432.tcp())
.with_wait_for(WaitFor::message_on_stdout(
"database system is ready",
));

let container = image
.with_env_var("POSTGRES_USER", "testuser")
.with_env_var("POSTGRES_PASSWORD", "testpass")
.with_env_var("POSTGRES_DB", "testdb")
.start()
.await
.expect("Failed to start container");

let host_port = container.get_host_port_ipv4(5432).await.unwrap();
let url = format!("postgres://testuser:testpass@localhost:{}/testdb", host_port);

(container, url)
}

Benefits:

  • Zero setup — tests start containers automatically
  • Isolation — each test gets fresh state
  • Realistic — tests run against real databases

Interface Contract Tests

Verify all storage implementations behave identically. Written in Gherkin:

# Run against SQLite (fast, no containers)
just test-interfaces

# Run against PostgreSQL (testcontainers)
just test-interfaces-postgres

# Run against Redis (testcontainers)
just test-interfaces-redis

Running Tests

# Unit tests
just test

# Integration tests
just integration

# Acceptance tests
just acceptance

# Interface contract tests
just test-interfaces
just test-interfaces-all

Direct commands

# Unit tests
cargo test --lib

# Storage integration tests
cargo test --test storage_postgres --features postgres
cargo test --test storage_redis --features redis

# Acceptance tests
cargo test --test acceptance

Next Steps