Testing
Grove has a built-in test runner that lets you verify actions, validation rules, state machines, and workflows without spinning up a server. Tests live alongside the code they exercise, and
manzano testdiscovers and runs them automatically.
Prerequisites: Familiarity with modules, records, events, and actions. The Build a Task Tracker tutorial walks through a complete module with tests.
What you'll learn:
- Where test files live and how they are discovered
- The syntax for writing test cases and steps
- How to construct request blocks with meta fields
- How to assert on record state and emitted events
- How to test workflows with mocked activities
- How to run tests from the CLI
File Convention
Test files use the suffix _test.grove and live in the same module
directory as the code they test -- not in a separate test module.
modules/
organization/
module.grove
organization_test.grove <-- tests for this module
manzano test recursively discovers every *_test.grove file in your
project and runs all the tests it contains. No registration or
configuration is required.
Test Syntax
A test is declared with the test keyword, a name, and a body containing
one or more run steps. The name can be a bare identifier or a quoted
string.
test organization_lifecycle {
run create { ... }
run update { ... }
}
test "spaces in the name work too" {
run "first step" { ... }
}
Each run step executes in order. State from earlier steps carries
forward, so you can build up a full entity lifecycle across multiple
steps.
Request Block
Each run step sends a request that specifies the action to invoke, its
parameters, and metadata. The canonical form uses the object syntax:
request {
name: "action_name",
params: {
field: value
},
meta: {
id: "record-id",
by: "user-id",
timestamp: 1700000000000,
action_id: "cmd-1"
}
}
The name field identifies which action to call. params carries the
action's input fields. meta provides the context that the runtime
normally supplies in production.
Meta Fields
Every request requires a meta block with four fields:
| Field | Purpose | Rules |
|---|---|---|
id | Record identity | Use the same value across steps that target the same record |
by | Principal (who is performing the action) | Any string; often "admin" or a user ID |
timestamp | Epoch milliseconds | Increment between steps so events are ordered |
action_id | Unique command identifier | Must be unique across every step in the test |
A common convention is to start timestamp at 1700000000000 and add
100 per step, and to prefix action_id with cmd- followed by a
descriptive slug.
Expect and Assert
After the request block you declare the expected outcome and any assertions about the resulting state.
Expect
expect declares whether the step should succeed or fail:
expect ok # action must succeed
expect error # action must fail (any validator)
expect error "name_not_empty" # action must fail with this specific validator
Assert
assert checks record state and emitted events after a successful step:
assert record.name == "Acme Corp"
assert record.status == Status.active
assert events.len == 1
assert events[0].name == "created"
You can combine multiple assertions in a single step. Every assertion must pass for the step to be considered successful.
Duration Gotcha
When asserting on duration fields, use the string form with quotes, not a bare duration literal:
# correct
assert record.timeout == "2m"
# incorrect -- bare 2m is not valid in an assertion context
assert record.timeout == 2m
Complete Example
The following test exercises a full organization lifecycle -- create, update, and delete -- then verifies that validation rejects an empty name.
module organization_test
test organization_lifecycle {
run create {
request {
name: "create_organization",
params: {
name: "Acme Corp"
},
meta: {
id: "org-1",
by: "admin",
timestamp: 1700000000000,
action_id: "cmd-create-org"
}
}
expect ok
assert record.name == "Acme Corp"
assert events.len == 1
assert events[0].name == "created"
}
run update {
request {
name: "update_organization",
params: {
name: "Acme Corporation"
},
meta: {
id: "org-1",
by: "admin",
timestamp: 1700000000100,
action_id: "cmd-update-org"
}
}
expect ok
assert record.name == "Acme Corporation"
assert events[0].name == "updated"
}
run delete {
request {
name: "delete_organization",
params: {},
meta: {
id: "org-1",
by: "admin",
timestamp: 1700000000200,
action_id: "cmd-delete-org"
}
}
expect ok
assert events[0].name == "deleted"
}
}
test organization_name_required {
run empty_name {
request {
name: "create_organization",
params: {
name: ""
},
meta: {
id: "org-bad",
by: "admin",
timestamp: 1700000001000,
action_id: "cmd-bad-org"
}
}
expect error "name_not_empty"
}
}
Key things to notice:
- Same
id("org-1") across create, update, and delete -- they target the same record. - Increasing
timestampvalues so events are ordered correctly. - Unique
action_idper step. - The validation test uses
expect error "name_not_empty"to pin the failure to a specific validator.
Workflow Tests
Workflow tests verify durable execution logic. Because workflows call
external activities, tests use mock blocks to stub those calls.
test "folder delete reparents children" {
workflow DeleteFolderReparent v1
run "folder with child folders" {
input {
folder_id: "folder-b",
organization_id: "org-1",
new_parent: "folder-a",
new_depth: 0
}
mock children {
return { folder_ids: ["folder-c"] }
}
mock reparent_folders { return { status: "moved" } }
expect ok
}
}
The workflow declaration at the top of the test names the workflow and
version under test. Each mock block matches an activity call by name
and returns a fixed response. This lets you test branching logic, error
handling, and compensation paths without real external services.
Running Tests
Run all tests
manzano test
This discovers every *_test.grove file in the project and runs all
tests. The CLI prints each test and step with a pass/fail indicator.
JSON output
manzano test --json
Produces machine-readable JSON output, useful for CI pipelines. Each test result includes the test name, step names, pass/fail status, and any assertion or expectation failures.
Typical output
modules/organization/organization_test.grove
organization_lifecycle
create .............. ok
update .............. ok
delete .............. ok
organization_name_required
empty_name .......... ok
4 passed, 0 failed
See Also
- CLI Reference -- full list of CLI commands and flags
- Build a Task Tracker -- end-to-end tutorial that includes tests
- Validation & State Machines -- writing the rules that tests verify