Comprehensive testing patterns and anti-patterns for writing and reviewing tests
Scanned 9/2/2026
Install to Claude Code
npx -y skills add fagemx/edda --skill testing --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Testing?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/fagemx-testing)More formats (shields.io, HTML) on the badges page.
---
name: testing
description: Comprehensive testing patterns and anti-patterns for writing and reviewing tests
context: fork
---
# Testing Skill
Use this skill when writing tests, reviewing test code, or investigating test failures.
## Documentation
Read the relevant reference based on context:
| Context | Reference |
|---------|-----------|
| General testing | This document |
| Anti-patterns | [Anti-Patterns](#anti-patterns) section below |
| Patterns | [Patterns](#patterns) section below |
| CLI testing (`edda-cli`) | [CLI Testing](#cli-testing) section below |
| Bridge testing (`edda-bridge-claude`) | [Bridge Testing](#bridge-testing) section below |
| Ledger testing (`edda-ledger`) | [Ledger Testing](#ledger-testing) section below |
| Store testing (`edda-store`) | [Store Testing](#store-testing) section below |
## Key Principles
1. **Integration tests are primary** — test at system entry points (CLI commands, hook dispatch)
2. **Mock at the boundary** — only mock external services, not internal code
3. **Use real infrastructure** — real filesystem (tempdir), real ledger, real coordination log
4. **Test behavior, not implementation** — verify outcomes, not internal function calls
## Test Organization
This project uses **inline tests** exclusively — zero `tests/` directories across all crates.
```
crates/edda-cli/
src/
cmd_bridge.rs # #[cfg(test)] mod tests { ... }
cmd_gc.rs # #[cfg(test)] mod tests { ... }
crates/edda-bridge-claude/
src/
dispatch.rs # #[cfg(test)] mod tests { ... } (40+ tests)
peers.rs # #[cfg(test)] mod tests { ... } (25+ tests)
crates/edda-ledger/
src/
ledger.rs # #[cfg(test)] mod tests { ... }
```
**Convention:** All tests live inside `#[cfg(test)] mod tests` at the bottom of the source file. Never create separate `tests/` directories.
## Commands
```bash
# Run all tests in workspace
cargo test --workspace
# Run tests for a specific crate
cargo test -p edda-bridge-claude
# Run a specific test by name
cargo test -p edda-bridge-claude -- cross_session_binding
# Run tests matching a pattern
cargo test -- session
# Run tests with output visible
cargo test -- --nocapture
# Run tests with backtrace on failure
RUST_BACKTRACE=1 cargo test -p edda-cli
```
## Setup Pattern
Every test module uses a consistent `setup_workspace()` helper with `AtomicU64` counter for unique temp dirs:
```rust
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
fn setup_workspace() -> (std::path::PathBuf, edda_ledger::Ledger) {
let n = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let tmp = std::env::temp_dir().join(format!(
"edda_test_{}_{n}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&tmp);
let paths = edda_ledger::EddaPaths::discover(&tmp);
edda_ledger::ledger::init_workspace(&paths).unwrap();
edda_ledger::ledger::init_head(&paths, "main").unwrap();
edda_ledger::ledger::init_branches_json(&paths, "main").unwrap();
let ledger = edda_ledger::Ledger::open(&tmp).unwrap();
(tmp, ledger)
}
#[test]
fn my_test() {
let (tmp, ledger) = setup_workspace();
// ... test logic ...
let _ = std::fs::remove_dir_all(&tmp);
}
}
```
**Key points:**
- `AtomicU64` counter prevents collisions between parallel tests
- `std::process::id()` prevents collisions between test binaries
- Always clean up temp dirs at end of test
- For store-based tests, also clean `edda_store::project_dir(pid)`
## Patterns
### Use `edda_store` for State Tests
```rust
#[test]
fn test_peer_coordination() {
let pid = "test_unique_name";
let _ = std::fs::remove_dir_all(edda_store::project_dir(pid));
let _ = edda_store::ensure_dirs(pid);
// Write coordination events
crate::peers::write_binding(pid, "s1", "auth", "db.engine", "postgres");
// Verify via public API
let conflict = crate::peers::find_binding_conflict(pid, "db.engine", "OTHER");
assert!(conflict.is_some());
let _ = std::fs::remove_dir_all(edda_store::project_dir(pid));
}
```
### Use `setup_workspace()` for Ledger Tests
```rust
#[test]
fn test_ledger_event() {
let (tmp, ledger) = setup_workspace();
let branch = ledger.head_branch().unwrap();
let parent = ledger.last_event_hash().unwrap();
let tags = vec!["decision".to_string()];
let mut event = edda_core::event::new_note_event(
&branch, parent.as_deref(), "system", "db.engine: postgres", &tags,
).unwrap();
edda_core::event::finalize_event(&mut event);
ledger.append_event(&event, false).unwrap();
let events = ledger.iter_events().unwrap();
assert_eq!(events.len(), 1);
let _ = std::fs::remove_dir_all(&tmp);
}
```
### Use Environment Variables for Config Overrides
```rust
#[test]
fn test_with_custom_threshold() {
// Override config via env var
std::env::set_var("EDDA_PEER_STALE_SECS", "0");
// ... test logic ...
// Always clean up env vars
std::env::remove_var("EDDA_PEER_STALE_SECS");
}
```
### Test Multi-Session Scenarios
```rust
#[test]
fn test_cross_session() {
let pid = "test_cross_session";
let _ = std::fs::remove_dir_all(edda_store::project_dir(pid));
let _ = edda_store::ensure_dirs(pid);
// Simulate multiple sessions with heartbeats
let signals = crate::signals::SessionSignals::default();
crate::peers::write_heartbeat(pid, "s1", &signals, Some("auth"));
crate::peers::write_heartbeat(pid, "s2", &signals, Some("billing"));
// Write data from one session
crate::peers::write_binding(pid, "s1", "auth", "key", "value");
// Verify other session can see it
let updates = crate::peers::render_peer_updates(pid, "s2").unwrap();
assert!(updates.contains("value"));
crate::peers::remove_heartbeat(pid, "s1");
crate::peers::remove_heartbeat(pid, "s2");
let _ = std::fs::remove_dir_all(edda_store::project_dir(pid));
}
```
## Anti-Patterns
### AP-1: Don't Create `tests/` Directories
```rust
// BAD: Creating tests/integration_test.rs
// This project uses inline tests exclusively
// GOOD: Add tests inside the source file
#[cfg(test)]
mod tests {
use super::*;
// tests here have access to private functions
}
```
### AP-2: Don't Mock Internal Code
```rust
// BAD: Creating trait abstractions just for testing
trait Storage { fn write(&self, key: &str, value: &str); }
struct MockStorage; // Don't do this
// GOOD: Test through the real code path
#[test]
fn test_decide_writes_binding() {
let (tmp, _ledger) = setup_workspace();
decide(&tmp, "db.engine=postgres", Some("need JSONB"), None).unwrap();
// Verify via public API
let conflict = find_binding_conflict(&pid, "db.engine", "OTHER");
assert!(conflict.is_some());
}
```
### AP-3: Don't Test Implementation Details
```rust
// BAD: Asserting internal file structure
assert!(fs::read_to_string(path).unwrap().contains("specific_internal_format"));
// GOOD: Test through public API behavior
let result = decide(&tmp, "db.engine=postgres", None, None);
assert!(result.is_ok());
let events = ledger.iter_events().unwrap();
assert_eq!(events.len(), 1);
```
### AP-4: Don't Leak Test State
```rust
// BAD: Using fixed project IDs that collide
let pid = "test"; // Will collide with other tests!
// GOOD: Use unique IDs and clean up
let pid = "test_specific_scenario_name";
let _ = fs::remove_dir_all(edda_store::project_dir(pid)); // Clean first
let _ = edda_store::ensure_dirs(pid);
// ... test ...
let _ = fs::remove_dir_all(edda_store::project_dir(pid)); // Clean after
```
### AP-5: Don't Forget Env Var Cleanup
```rust
// BAD: Setting env var without cleanup
std::env::set_var("EDDA_SESSION_ID", "test");
// test runs... but env var leaks to other tests!
// GOOD: Always remove env vars
std::env::set_var("EDDA_SESSION_ID", "test");
// ... test logic ...
std::env::remove_var("EDDA_SESSION_ID");
```
### AP-6: Don't Use Sleep Without Justification
```rust
// BAD: Arbitrary sleep
std::thread::sleep(Duration::from_secs(5));
// GOOD: Sleep only when testing time-dependent behavior (e.g., stale detection)
// and use the minimum necessary duration
std::thread::sleep(Duration::from_millis(1100)); // Must exceed 1s mtime granularity
std::env::set_var("EDDA_PEER_STALE_SECS", "0");
```
## CLI Testing
Tests for `edda-cli` commands live in each `cmd_*.rs` file.
**Key patterns:**
- Use `setup_workspace()` for commands that need a ledger
- Use `edda_store::ensure_dirs()` for commands that need store state
- Set `EDDA_SESSION_ID` and `EDDA_SESSION_LABEL` env vars for session-aware commands
- Call the command function directly (e.g., `decide()`, `peers()`) — don't spawn a process
**Example crates:**
- `cmd_bridge.rs`: 6 tests covering `decide()`, `find_prior_decision()`, `resolve_session_id()`
- `cmd_gc.rs`: Tests for garbage collection logic
- `cmd_blob.rs`: Tests for blob management
- `cmd_draft.rs`: Tests for draft operations
## Bridge Testing
Tests for `edda-bridge-claude` are the most extensive (~230 tests).
**Key patterns:**
- `dispatch.rs`: Tests hook dispatch flow (`dispatch_session_start`, `dispatch_user_prompt_submit`, `dispatch_session_end`)
- `peers.rs`: Tests coordination protocol (`write_binding`, `write_claim`, `compute_board_state`, `render_peer_updates`)
- Use `crate::peers::write_heartbeat()` to simulate multi-session scenarios
- Use `crate::peers::write_binding()` to set up coordination state
**Access notes:**
- Inline tests can access `pub(crate)` functions (e.g., `compute_board_state`, `write_heartbeat`)
- Cross-crate tests must use `pub` functions only (e.g., `find_binding_conflict`)
## Ledger Testing
Tests for `edda-ledger` cover event creation, appending, querying, and branch operations.
**Key patterns:**
- Always init workspace before testing: `init_workspace()`, `init_head()`, `init_branches_json()`
- Use `Ledger::open()` to get a ledger handle
- Use `ledger.iter_events()` to verify event contents
- Events are immutable once appended — test creation, not modification
## Store Testing
Tests for `edda-store` cover project directory management and atomic writes.
**Key patterns:**
- `project_id()` is deterministic (blake3 hash of normalized path)
- `ensure_dirs()` creates the full directory tree
- `write_atomic()` for safe file writes
- Always clean up `project_dir()` after tests
---
## Checklist for New Tests
Before submitting:
- [ ] Test is inside `#[cfg(test)] mod tests` in the source file
- [ ] Uses unique project ID or temp dir (no collisions)
- [ ] Cleans up temp dirs and store directories
- [ ] Cleans up env vars
- [ ] Tests behavior through public/crate API, not internal details
- [ ] Passes `cargo test --workspace` with zero failures
- [ ] No `#[ignore]` without explanation
## References
- Code quality skill: `.claude/skills/code-quality/SKILL.md`
- Project principles: `.claude/skills/project-principles/SKILL.md`
- CLAUDE.md testing conventions
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!