Maestro Deck
Guides

Test data management

Manage test data, fixtures, and environments for reliable Maestro flows across local development and CI.

Most "flaky test" stories are actually "shared test data" stories. Two flows mutate the same row, two CI jobs spawn the same user, a developer's local seed drifts from staging — the flow looks broken but the test data is. This guide is the playbook for keeping test data sane.

TL;DR

Generate unique data per run, never reuse. Reset state at the suite boundary, not between flows. Keep credentials in environment variables, not in YAML. Use env in flow config to pass values, and a .env.test file at the repo root for defaults.

Where test data comes from

In a Maestro flow, "test data" is one of:

  1. Hard-coded constantsinputText: "qa@example.com". Simplest, brittlest.
  2. env variables — read from ${EMAIL} at runtime. Better.
  3. Generated at run time — a UUID, a timestamp. Best for write paths.
  4. Fetched from a fixture API — a per-job pre-seeded user. Most robust.

You will use all four. The trick is matching the strategy to the path.

Generating unique data

For any flow that creates a record, generate a unique value:

appId: com.example.app
env:
  EMAIL: "qa-${RUN_ID}@example.com"
---
- launchApp
- tapOn: "Sign up"
- inputText: ${EMAIL}
- inputText: "ValidPassword!1"
- tapOn: "Submit"

RUN_ID is set by your CI job (${{ github.run_id }}, ${CI_PIPELINE_ID}, etc). Locally, fall back to a timestamp:

RUN_ID=${RUN_ID:-$(date +%s)} maestro test .maestro/

Now two parallel runs can never collide on email.

Reading users from the environment

For flows that need an existing user (a logged-in customer with prior orders, say), don't hard-code credentials:

appId: com.example.app
env:
  EMAIL: ""
  PASSWORD: ""
---
- launchApp
- tapOn: "Sign in"
- inputText: ${EMAIL}
- inputText: ${PASSWORD}
- tapOn: "Submit"

Provide values at run time:

EMAIL=qa-fixture@example.com PASSWORD=$QA_PASSWORD maestro test .maestro/

Store QA_PASSWORD in CI secrets. Never commit credentials.

Resetting state

For flows that depend on a pristine starting state, reset at the boundary, not in every flow:

# Suite entrypoint
curl -X POST https://staging.example.com/_test/reset
maestro test .maestro/

Mid-suite resets are tempting and almost always wrong: they make flow ordering significant, break parallelism, and hide the real problem (flows that aren't independent).

Fixture APIs

The most robust setup, used by every team running Maestro at scale:

  1. Build a _test namespace into your backend (gated by env / feature flag).
  2. Expose endpoints like POST /_test/users that return a fresh user.
  3. Each flow's first step calls the endpoint via runScript:
- runScript: |
    output.user = http.post('https://staging.example.com/_test/users', {})
- inputText: ${output.user.email}

This eliminates almost every shared-state flake.

Per-environment defaults

A .env.test file at the repo root holds non-secret defaults:

API_URL=https://staging.example.com
LOCALE=en_US

Load it in your runner script:

set -a; source .env.test; set +a
maestro test .maestro/

Override per-CI-job for prod-like environments.

App build under test

Mismatched test data is often a mismatched app build problem. Pin the build to the test data version:

  • The app build under test should hit API_URL (set at build time, not via runtime config that the user can change).
  • The fixture data should be seeded at the same version the app expects.
  • Schema migrations on the backend break the app build and the flows. Coordinate via your release pipeline.

Cleaning up

Every test run should be cleanable. The pattern:

- runScript: |
    output.cleanup_id = http.post('/_test/users', {}).id
- ...flow steps...
- runScript: |
    http.delete('/_test/users/' + output.cleanup_id)

Even better: tag fixture records with the run ID and run a nightly job that purges anything older than 7 days. Manual cleanup never happens.

Sensitive data

  • Don't put real PII in test fixtures. Use Faker.
  • Don't inputText real card numbers. Use the network's test PAN.
  • Treat any test user with admin scope as a production user — it has access to real data, even if it's "test" data.

Common pitfalls

  • One shared QA account. Every flow logs in as qa@example.com. The first flow's session corrupts the second. Generate per-flow.
  • Local DB drift. Developer's local DB has 3-month-old seeds; staging was reset last week. Flows pass locally, fail in CI. Fix: nightly local seed reset as part of make dev-up.
  • Coupling flow order. "Run create-user before login-user." Don't. Make every flow self-contained.

On this page