Installation
Get scaf installed and configured.
scaf is a declarative domain-specific language (DSL) for database test scaffolding. It provides a human-readable syntax for defining database queries and their test cases, with support for hierarchical test organization, setup blocks, input/output specifications, and post-execution assertions.
Testing database queries typically involves:
scaf solves these problems by providing:
$param: value for inputs, field: expected for outputsflowchart TB
subgraph scafFile[".scaf File"]
queries["query definitions"]
setup["setup / teardown"]
tests["tests & groups"]
end
subgraph CLI["scaf CLI"]
parser["Parser"]
runner["Test Runner"]
generator["Code Generator"]
end
subgraph Dialect["Dialect (Query Language)"]
cypher["Cypher"]
sql["SQL"]
end
subgraph Database["Database (Execution)"]
neo4j["Neo4j"]
postgres["PostgreSQL"]
mysql["MySQL"]
end
subgraph Adapter["Adapter (Code Generation)"]
neogo["neogo"]
pgx["pgx"]
end
scafFile --> parser
parser --> runner
parser --> generator
runner --> Dialect
Dialect --> Database
generator --> Adapter
cypher -.-> neo4j
sql -.-> postgres
sql -.-> mysql
neogo -.-> neo4j
pgx -.-> postgres
scaf separates several concerns:
This means multiple databases can share a dialect (PostgreSQL, MySQL, and SQLite all use SQL), and code generation uses adapters matched to each database.
Define named database queries that your tests will exercise:
fn GetUser `MATCH (u:User {id: $userId})RETURN u.name, u.email, u.age`
fn CreatePost `CREATE (p:Post {title: $title, authorId: $authorId})RETURN p`Queries use backticks to contain the raw database query in your dialect (Cypher, SQL, etc.).
Group tests by the query they’re testing:
GetUser { test "finds user by id" { $userId: 1
u.name: "Alice" }}The scope name must match a defined query. All tests within inherit the query context.
Each test specifies:
$test "user is a verified adult" { $userId: 1
u.name: "Alice" u.verified: true
assert (u.age >= 18)}Organize related tests and share setup:
GetUser { group "existing users" { setup `CREATE (:User {id: 1, name: "Alice"})`
test "finds Alice" { $userId: 1 u.name: "Alice" }
test "finds Bob" { $userId: 2 u.name: "Bob" } }
group "edge cases" { test "returns null for missing user" { $userId: 999 u.name: null } }}Setup blocks run before tests, teardown runs after. They can be defined at multiple levels:
// Suite-level setupsetup `CREATE (alice:User {id: 1, name: "Alice"})`
GetUser { // Scope-level setup setup `CREATE (:Post {authorId: 1})`
group "with posts" { // Group-level setup setup `CREATE (:Comment {postId: 1})`
test "user with activity" { // Test-level setup setup `CREATE (:Like {userId: 1})`
$userId: 1 // ... } }}Tests inherit all setup from their ancestors, executed in order: Suite → Scope → Group → Test.
Beyond simple output matching, use assert blocks for complex validations:
test "user data validation" { $userId: 1
u.name: "Alice"
// Expression assertions assert { (u.age >= 18) (u.email contains "@") (len(u.name) > 0) }
// Query assertions - run another query and check results assert `MATCH (p:Post {authorId: 1}) RETURN count(p) as cnt` { (cnt > 0) }}A typical scaf file has this structure:
// 1. Imports (optional)import fixtures "./shared/fixtures"
// 2. Query definitionsfn GetUser `...`fn CreatePost `...`
// 3. Global setup & teardownsetup `...`teardown `...`
// 4. Query scopes with testsGetUser { group "scenario" { test "case" { ... } }}
CreatePost { test "creates post" { ... }}Installation
Get scaf installed and configured.
Quick Start
Write your first test file.