Skip to content

Setup & Teardown

Setup blocks prepare the database state before tests run. Teardown blocks clean up after tests complete. scaf supports setup at multiple levels with inheritance.

The simplest form — a raw query in backticks:

setup `
CREATE (alice:User {id: 1, name: "Alice", email: "alice@example.com"})
CREATE (bob:User {id: 2, name: "Bob", email: "bob@example.com"})
`

Multi-statement setup:

setup `
// Clear existing data
MATCH (n) DETACH DELETE n
// Create test users
CREATE (alice:User {id: 1, name: "Alice", email: "alice@example.com", age: 30})
CREATE (bob:User {id: 2, name: "Bob", email: "bob@example.com", age: 25})
// Create relationships
CREATE (alice)-[:FRIENDS_WITH]->(bob)
`

Runs after tests complete (in reverse order of setup):

teardown `
MATCH (n) DETACH DELETE n
`

Setup can be defined at four levels:

Runs once at the start of the file:

// This runs first, before any scope
setup `
CREATE (:Config {env: "test"})
CREATE (alice:User {id: 1, name: "Alice"})
CREATE (bob:User {id: 2, name: "Bob"})
`
GetUser {
test "uses suite data" {
$userId: 1
u.name: "Alice" // Created by suite setup
}
}

Runs for each function scope:

GetUser {
// Runs before any test in this scope
setup `
CREATE (:Session {userId: 1, token: "abc123"})
`
test "user has session" { ... }
test "another test" { ... }
}
CreateUser {
// Different setup for this scope
setup `
CREATE (:Sequence {name: "user_id", value: 100})
`
test "creates user" { ... }
}

Runs for each group:

GetUser {
group "with posts" {
setup `
CREATE (:Post {id: 1, authorId: 1, title: "First Post"})
CREATE (:Post {id: 2, authorId: 1, title: "Second Post"})
`
test "user has posts" { ... }
test "post count" { ... }
}
group "without posts" {
// No setup — user has no posts
test "new user" { ... }
}
}

Runs only for a specific test:

CountUsers {
test "base count" {
count: 2
}
test "with extra user" {
setup `CREATE (:User {id: 999, name: "Temp"})`
count: 3
}
test "back to base count" {
// Previous test's setup rolled back
count: 2
}
}

Tests inherit setup from all ancestor levels. Execution order:

Suite Setup → Scope Setup → Group Setup → Test Setup

Example:

// 1. Suite setup
setup `CREATE (:App {name: "test"})`
GetUser {
// 2. Scope setup
setup `CREATE (:User {id: 1, name: "Alice"})`
group "with profile" {
// 3. Group setup
setup `CREATE (:Profile {userId: 1, bio: "Hello"})`
test "full setup chain" {
// 4. Test setup
setup `CREATE (:Preference {userId: 1, theme: "dark"})`
// All four setups have run at this point
$userId: 1
u.name: "Alice"
}
}
}

Reference setups defined elsewhere:

Run an imported module’s setup clause:

import fixtures "./shared/fixtures"
// Use module setup (runs the module's setup clause)
setup fixtures

Call a specific function from an imported module:

import fixtures "./shared/fixtures"
// Call function with parameters (no $ prefix)
setup fixtures.CreateUsers()
import fixtures "./shared/fixtures"
// Pass parameters to setup (no $ prefix on param names)
setup fixtures.CreatePosts(count: 10, authorId: 1)
GetUser {
setup fixtures.AddFriends(userId: 1, friendIds: [2, 3, 4])
test "user with friends" {
$userId: 1
// ...
}
}

Combine multiple setup calls:

setup {
fixtures // run module's setup clause
fixtures.CreateUser(id: 1, name: "Test")
`CREATE (:Extra)` // inline query
}

In shared/fixtures.scaf:

fn CreateUsers() `
CREATE (alice:User {id: 1, name: "Alice"})
CREATE (bob:User {id: 2, name: "Bob"})
`
fn CreatePosts(count: int, authorId: int) `
UNWIND range(1, $count) as i
CREATE (:Post {id: i, authorId: $authorId, title: "Post " + i})
`
fn AddFriends(userId: int, friendIds: [int]) `
MATCH (u:User {id: $userId})
UNWIND $friendIds as friendId
MATCH (f:User {id: friendId})
CREATE (u)-[:FRIENDS_WITH]->(f)
`

By default, each test runs in a transaction that rolls back:

CountUsers {
test "creates temp user - rolls back" {
setup `CREATE (:User {id: 999})`
count: 3 // Sees temp user
}
test "temp user is gone" {
count: 2 // Transaction rolled back
}
}

For persistent cleanup:

setup `
// Create data that persists
CREATE (:PersistentNode)
`
teardown `
// Explicitly clean up
MATCH (n:PersistentNode) DELETE n
`

Use multiple groups with different setup:

GetUser {
group "verified users" {
setup `CREATE (:User {id: 1, verified: true})`
test "verified flag is true" {
$userId: 1
u.verified: true
}
}
group "unverified users" {
setup `CREATE (:User {id: 1, verified: false})`
test "verified flag is false" {
$userId: 1
u.verified: false
}
}
}

Build on previous setup:

setup `CREATE (:User {id: 1, name: "Alice"})`
GetUser {
setup `
MATCH (u:User {id: 1})
SET u.verified = true
`
group "with posts" {
setup `
MATCH (u:User {id: 1})
CREATE (u)-[:AUTHORED]->(:Post {title: "Post 1"})
`
test "user has verified status and posts" {
$userId: 1
u.verified: true
}
}
}
  1. Keep setup focused — Only set up what’s needed for the tests

  2. Use inheritance — Move common setup to higher levels

  3. Prefer transactions — Let rollback handle cleanup

  4. Name setups clearly — If importing, use descriptive function names

  5. Document complex setup — Add comments for non-obvious data

// Good: Clear, minimal, commented
setup `
// Base users for all tests
CREATE (alice:User {id: 1, name: "Alice", role: "admin"})
CREATE (bob:User {id: 2, name: "Bob", role: "user"})
// Admin has special permissions
CREATE (alice)-[:HAS_PERMISSION]->(:Permission {name: "delete"})
`
GetUser {
// Only add what this scope needs
setup `CREATE (:Session {userId: 1})`
group "with activity" {
// Only add what this group needs
setup `CREATE (:Login {userId: 1, time: timestamp()})`
test "has recent login" { ... }
}
}