Tests & Groups
Tests & Groups
Section titled “Tests & Groups”scaf provides a hierarchical structure for organizing tests: scopes group tests by function, groups organize related scenarios, and tests define individual cases.
Test Scopes
Section titled “Test Scopes”Every test must be inside a scope that matches a defined function:
fn GetUser(userId) `MATCH (u:User {id: $userId})RETURN u.name, u.email`
// Scope name must match function nameGetUser { test "finds user" { $userId: 1 u.name: "Alice" }}Multiple Scopes
Section titled “Multiple Scopes”You can have multiple scopes in one file:
fn GetUser(userId) `...`fn CreateUser(name, email) `...`fn DeleteUser(userId) `...`
GetUser { test "finds user" { ... }}
CreateUser { test "creates user" { ... }}
DeleteUser { test "deletes user" { ... }}Groups
Section titled “Groups”Groups organize related tests within a scope:
GetUser { group "existing users" { test "finds Alice" { $userId: 1 u.name: "Alice" }
test "finds Bob" { $userId: 2 u.name: "Bob" } }
group "edge cases" { test "missing user" { $userId: 999 u.name: null } }}Nested Groups
Section titled “Nested Groups”Groups can be nested for deeper organization:
GetUser { group "by role" { group "admin users" { test "admin with full access" { $userId: 1 u.role: "admin" u.canDelete: true } }
group "regular users" { test "user with limited access" { $userId: 2 u.role: "user" u.canDelete: false } } }}Group Setup
Section titled “Group Setup”Groups can have their own setup that applies to all contained tests:
GetUser { group "with posts" { setup ` CREATE (u:User {id: 1, name: "Alice"}) CREATE (p1:Post {id: 1, authorId: 1, title: "Post 1"}) CREATE (p2:Post {id: 2, authorId: 1, title: "Post 2"}) `
test "user has posts" { $userId: 1 u.name: "Alice" }
test "post count is correct" { $userId: 1 // ... } }}Tests are the core unit. Each test specifies:
Input Parameters
Section titled “Input Parameters”Prefixed with $:
test "finds user by id" { $userId: 1 $includeDeleted: false
// ...}Expected Outputs
Section titled “Expected Outputs”Field paths and their expected values:
test "returns all user fields" { $userId: 1
u.name: "Alice" u.email: "alice@example.com" u.age: 30 u.verified: true u.tags: ["admin", "beta-tester"] u.metadata: {theme: "dark", notifications: true}}Test-Level Setup
Section titled “Test-Level Setup”Tests can have their own setup:
test "with temp data" { setup `CREATE (:TempNode {id: 999})`
$userId: 1 u.name: "Alice"}Assertions
Section titled “Assertions”Expression-based or query-based validations:
test "user meets requirements" { $userId: 1
u.name: "Alice"
assert { (u.age >= 18) (u.verified == true) }}Test Execution
Section titled “Test Execution”Setup Inheritance
Section titled “Setup Inheritance”Tests inherit setup from all ancestors:
// 1. Suite setup runs firstsetup `CREATE (:Config {key: "value"})`
GetUser { // 2. Scope setup runs second setup `CREATE (:User {id: 1, name: "Alice"})`
group "with posts" { // 3. Group setup runs third setup `CREATE (:Post {authorId: 1})`
test "user with post" { // 4. Test setup runs last setup `CREATE (:Comment {postId: 1})`
$userId: 1 u.name: "Alice" } }}The execution order is: Suite → Scope → Group → Test.
Transaction Isolation
Section titled “Transaction Isolation”Each test runs in its own transaction that rolls back after completion:
CountUsers { test "base count is 2" { count: 2 }
test "temp user in transaction" { setup `CREATE (:User {id: 999})` count: 3 // Sees the temp user }
test "still 2 after rollback" { count: 2 // Previous test's data was rolled back }}Teardown
Section titled “Teardown”Teardown blocks run in reverse order after tests complete:
setup `CREATE (:A)`teardown `MATCH (a:A) DELETE a`
GetUser { setup `CREATE (:B)` teardown `MATCH (b:B) DELETE b`
test "example" { // After this test: // 1. Scope teardown runs (delete B) // 2. Suite teardown runs (delete A) }}Test Naming
Section titled “Test Naming”Test names should be descriptive:
// Good: Describes the scenario and expectationtest "finds existing user by id" { ... }test "returns null for non-existent user" { ... }test "validates email format on creation" { ... }
// Avoid: Vague or technical namestest "test1" { ... }test "user query" { ... }test "happy path" { ... }Group names should describe the category:
// Good: Clear category namesgroup "existing users" { ... }group "authentication" { ... }group "error handling" { ... }
// Avoid: Overly genericgroup "tests" { ... }group "cases" { ... }Test Path
Section titled “Test Path”Each test has a unique path: FunctionScope/Group/Test:
GetUser { group "existing users" { test "finds Alice" { ... } // Path: GetUser/existing users/finds Alice }
test "missing user" { ... } // Path: GetUser/missing user}This path is used for:
- Test filtering with
--run - Verbose output
- Neotest integration
Filtering Tests
Section titled “Filtering Tests”Run specific tests using the --run flag:
# Run all tests in GetUser scopescaf test --run="GetUser"
# Run tests in a specific groupscaf test --run="GetUser/existing users"
# Run a specific testscaf test --run="GetUser/existing users/finds Alice"
# Pattern matchingscaf test --run="*/edge cases/*"Best Practices
Section titled “Best Practices”-
One assertion per concept — Keep tests focused
-
Descriptive names — Names should explain the scenario
-
Use groups for organization — Group related scenarios together
-
Leverage setup inheritance — DRY up common setup
-
Test edge cases — Include null, missing, and invalid inputs
GetUser { group "happy path" { test "finds user with all fields" { $userId: 1 u.name: "Alice" u.email: "alice@example.com" } }
group "edge cases" { test "missing user returns null" { $userId: 999 u.name: null }
test "handles negative id" { $userId: -1 u.name: null } }
group "validation" { test "age must be positive" { $userId: 1 assert { (u.age > 0) } } }}