Skip to content

One test, many environments — variables

A test that types qa@staging.example.com into a login form only works against staging. Copy it for production and you have two tests that drift apart.

Instead, write the test once with a placeholder:

type {{USERNAME}} into Email
type {{PASSWORD}} into Password
click Sign in
verify text Welcome back

…and give each environment its own values. The same test then runs against staging, integration and production, and the only thing that changes is which environment you name.

On the environment, because that is what varies. Not on the test set: three sets aimed at three environments would mean nine copies of the same two facts, and a test whose values live on a set is a test you cannot run on its own from the Tests page — which is the test you cannot debug on its own either.

  1. Set a value on an environment.

    Terminal window
    curl -X PUT https://api.marriska.com/api/v1/cli/environments/$ENV_ID/variables/USERNAME \
    -H "Authorization: Bearer $MARRISKA_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"value": "qa@staging.example.com"}'
  2. Mark a password secret. It is encrypted at rest, never returned by any read, and masked everywhere we persist or stream.

    Terminal window
    curl -X PUT https://api.marriska.com/api/v1/cli/environments/$ENV_ID/variables/PASSWORD \
    -H "Authorization: Bearer $MARRISKA_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"value": "…", "is_secret": true}'
  3. Check what each environment is missing, before anyone runs anything:

    Terminal window
    curl https://api.marriska.com/api/v1/cli/projects/$PROJECT_ID/variables \
    -H "Authorization: Bearer $MARRISKA_API_KEY"

    The required list is worked out from your tests — every {{NAME}} they reference — so it cannot go stale. Each environment then says which of those names it defined and which it is missing.

A coding agent does the same through MCP: project_variables, list_variables, set_variable, delete_variable.

Highest first:

  1. a one-off override for a single run (coming with the run dialog);
  2. the data row, for a data-driven test — a row’s value wins for its own iteration;
  3. the environment’s value;
  4. a default on the project’s descriptor, which is also where you can explain what a name is for.

A descriptor is documentation and a fallback. It does not declare that a name is required: that comes from the tests themselves.

A name nothing can fill now refuses the run

Section titled “A name nothing can fill now refuses the run”

Two names survive on purpose, and if your test refuses because of one of these, it is our bug and not your setup:

  • {{env:NAME}} — read from the environment of the machine your local agent runs on, against an allowlist that comes from that machine and never from us. Use it for a credential you will not store with us at all. Cloud runs cannot resolve it, because there is no machine of yours involved.
  • a name an earlier step produced through a call_api step’s saves. A request that logs in and saves TOKEN lets a later step use {{TOKEN}}. A step that references a name produced by a LATER step is still refused — it could never resolve.

A secret is encrypted with the same key as a stored session, never returned by a read, and masked in step logs, reports, the live run view and the generated code.

Secrecy belongs to the name, across the whole project. If PASSWORD is secret in staging, it is treated as secret on a production run too, even if production’s value was not marked. That is deliberate: the alternative is a value that is masked in one report and printed in the next.

A descriptor default is refused for a name that is secret anywhere, because a descriptor is readable by anyone who can read the project.

The run records which names it resolved and where each came from — the environment, or a default — and never the values. So you can see that a run used staging’s USERNAME and that PASSWORD was a secret, without the report becoming a place passwords live.