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 test discovers 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:


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:

FieldPurposeRules
idRecord identityUse the same value across steps that target the same record
byPrincipal (who is performing the action)Any string; often "admin" or a user ID
timestampEpoch millisecondsIncrement between steps so events are ordered
action_idUnique command identifierMust 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:


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