logos-blockchain-testing/book/src/testing-philosophy.md

156 lines
4.8 KiB
Markdown
Raw Normal View History

# Testing Philosophy
This framework embodies specific principles that shape how you author and run
scenarios. Understanding these principles helps you write effective tests and
interpret results correctly.
## Declarative over Imperative
Describe **what** you want to test, not **how** to orchestrate it:
```rust
// Good: declarative
ScenarioBuilder::topology()
.network_star()
.validators(2)
.executors(1)
.apply()
.transactions()
.rate(5) // 5 transactions per block
.apply()
.expect_consensus_liveness()
.build();
// Bad: imperative (framework doesn't work this way)
// spawn_validator(); spawn_executor();
// loop { submit_tx(); check_block(); }
```
**Why it matters:** The framework handles deployment, readiness, and cleanup.
You focus on test intent, not infrastructure orchestration.
## Protocol Time, Not Wall Time
Reason in **blocks** and **consensus intervals**, not wall-clock seconds.
**Consensus defaults:**
- Slot duration: 2 seconds (NTP-synchronized, configurable via `CONSENSUS_SLOT_TIME`)
- Active slot coefficient: 0.9 (90% block probability per slot)
- Expected rate: ~27 blocks per minute
```rust
// Good: protocol-oriented thinking
let plan = ScenarioBuilder::topology()
.network_star()
.validators(2)
.executors(1)
.apply()
.transactions()
.rate(5) // 5 transactions per block
.apply()
.with_run_duration(Duration::from_secs(60)) // Let framework calculate expected blocks
.expect_consensus_liveness() // "Did we produce the expected blocks?"
.build();
// Bad: wall-clock assumptions
// "I expect exactly 30 blocks in 60 seconds"
// This breaks on slow CI where slot timing might drift
```
**Why it matters:** Slot timing is fixed (2s by default, NTP-synchronized), so the
expected number of blocks is predictable: ~27 blocks in 60s with the default
0.9 active slot coefficient. The framework calculates expected blocks from slot
duration and run window, making assertions protocol-based rather than tied to
specific wall-clock expectations. Assert on "blocks produced relative to slots"
not "blocks produced in exact wall-clock seconds".
## Determinism First, Chaos When Needed
**Default scenarios are repeatable:**
- Fixed topology
- Predictable traffic rates
- Deterministic checks
**Chaos is opt-in:**
```rust
// Separate: functional test (deterministic)
let plan = ScenarioBuilder::topology()
.network_star()
.validators(2)
.executors(1)
.apply()
.transactions()
.rate(5) // 5 transactions per block
.apply()
.expect_consensus_liveness()
.build();
// Separate: chaos test (introduces randomness)
let chaos_plan = ScenarioBuilder::topology()
.network_star()
.validators(3)
.executors(2)
.apply()
.enable_node_control()
.chaos()
.restart()
.apply()
.transactions()
.rate(5) // 5 transactions per block
.apply()
.expect_consensus_liveness()
.build();
```
**Why it matters:** Mixing determinism with chaos creates noisy, hard-to-debug
failures. Separate concerns make failures actionable.
## Observable Health Signals
Prefer **user-facing signals** over internal state:
**Good checks:**
- Blocks progressing at expected rate (liveness)
- Transactions included within N blocks (inclusion)
- DA blobs retrievable (availability)
**Avoid internal checks:**
- Memory pool size
- Internal service state
- Cache hit rates
**Why it matters:** User-facing signals reflect actual system health.
Internal state can be "healthy" while the system is broken from a user
perspective.
## Minimum Run Windows
Always run long enough for **meaningful block production**:
```rust
// Bad: too short
.with_run_duration(Duration::from_secs(5)) // ~2 blocks (with default 2s slots, 0.9 coeff)
// Good: enough blocks for assertions
.with_run_duration(Duration::from_secs(60)) // ~27 blocks (with default 2s slots, 0.9 coeff)
```
**Note:** Block counts assume default consensus parameters:
- Slot duration: 2 seconds (configurable via `CONSENSUS_SLOT_TIME`)
- Active slot coefficient: 0.9 (90% block probability per slot)
- Formula: `blocks ≈ (duration / slot_duration) × active_slot_coeff`
If upstream changes these parameters, adjust your duration expectations accordingly.
The framework enforces minimum durations (at least 2× slot duration), but be explicit. Very short runs risk false confidence—one lucky block doesn't prove liveness.
## Summary
These principles keep scenarios:
- **Portable** across environments (protocol time, declarative)
- **Debuggable** (determinism, separation of concerns)
- **Meaningful** (observable signals, sufficient duration)
When authoring scenarios, ask: "Does this test the protocol behavior or
my local environment quirks?"