Skip to content

How to use test dependencies

This guide builds the Maxomart checkout test, one test that uses all five dependency kinds. The script calls a mocked payment API with an API key, then drives the Maxomart shop, which runs on your laptop, in a real browser and checks that the order is confirmed.

How to use test dependencies.

You need these entities before you start. Create each in its own product:

  • Secret PAYMENTS_API_KEY: The key the payment API expects.
  • Virtual service payments: POST /charges answers 201 with {"id": "...", "status": "approved"} only for Authorization: Bearer <the key>, and 401 otherwise. GET /health answers 200 with ok.
  • A named tunnel: it forwards to the Maxomart shop on your laptop.
  • Browser fleet eu-browsers: One Chrome browser. Created, not started.
  • Dataset checkout-users: A CSV with the columns email, first_name, last_name and city.
  • Test Maxomart checkout: A Selenium (Python) script test with 1 virtual user, 1 runner and a 45 second duration. A failure criterion fails the run when any iteration fails.
  • Test Payments health: An HTTP request builder test with one GET /health request.

The shop is a small local app with an Add to cart button, a Checkout button and an Order confirmed page. Its checkout handler calls the payment API with the same key.

A key pasted into a script is stored with the script and readable by everyone who can open it. A secret dependency keeps the value in the vault and injects it at run time.

  1. Open Secrets in the workspace and click Create secret. Name it PAYMENTS_API_KEY, paste the key as the Value and save. The value is never shown again.
  2. Open Maxomart checkout, go to Dependencies and click Add dependency.
  3. Select the Secret kind and pick PAYMENTS_API_KEY. Leave the suffix under Env name empty. The hint reads Injected as SECRET_PAYMENTS_API_KEY.
  4. Click Add.

The script reads SECRET_PAYMENTS_API_KEY from its environment. The console always adds the SECRET_ prefix. See Secrets for the naming rules.

The Dependencies tab with all five bindings in place. Each row lists the variables the script reads.

The checkout test must not charge a real card, and the real provider may rate limit a load test. The payments virtual service answers instead.

  1. On the Dependencies tab, click Add dependency and select the Virtual service kind.
  2. Pick payments and type PAYMENTS under Env var name. The hint reads Injected as MAXOPERF_VS_PAYMENTS_URL.
  3. Leave Auto-start with the run on and click Add.
The picker in the Add dependency dialog lists the workspace's virtual services with their state.

The service is stopped. That is fine: with auto-start on, the run deploys it and waits for it. See Virtual services.

Runners cannot reach localhost, so the shop needs a public address.

  1. Open Tunnels, click Create tunnel, name it and copy its client token.

  2. Start the shop, then connect the tunnel to its port in a terminal:

    Terminal window
    npx @maxoperf/tunnel http 3000 --token <your-tunnel-token> --name <your-tunnel-name>
  3. Wait for the tunnel to show Online in the console.

  4. On the test’s Dependencies tab, click Add dependency, select Tunnel, pick the tunnel and type SHOP. The hint reads Injected as MAXOPERF_TUNNEL_SHOP_URL. Click Add.

If you type http://localhost:3000 into a builder URL field, the builder warns “Runners can’t reach localhost” and offers a tunnel picker beside it. See Tunnels.

The script drives a real Chrome from the eu-browsers fleet instead of a browser on the runner.

  1. Click Add dependency and select the Browser fleet kind.
  2. Pick eu-browsers and type EU under Env var name. The hint shows the WebDriver variable, MAXOPERF_FLEET_EU_WEBDRIVER_URL. The fleet also injects the Playwright URL, the CDP URL and the token.
  3. Click Add.

The fleet is stopped. A bound fleet has no auto-start switch because the run always starts it. See Browser fleets for how the script connects.

A dataset is the one kind you bind outside the Dependencies tab.

  1. Open the test’s Data tab.
  2. Pick checkout-users under Bound Dataset and click Save binding. The tab shows Binding saved.
  3. Return to Dependencies. The dataset appears as a read-only row with a spreadsheet icon, and its name links back to the Data tab.
The Data tab with a dataset bound. Secrets are bound on the Dependencies tab instead.

Data is bound per virtual user. It is not a lease, so a run has no preparing step for it and nothing to start or stop. The checkout script in this example does not read the rows. To read them in a script, see Test data.

Every binding reaches the script as an environment variable. Here is the whole test script. It reads five of them, calls the payment API with the key, then buys through the shop in a fleet browser. It is a Selenium script in Python:

maxomart_dependencies.py
# The Maxomart checkout example. Runs on the MaxoPerf runner; every value below is
# injected by the test's dependencies (secret, virtual service, tunnel, browser fleet).
import json
import os
import urllib.request
from urllib.parse import quote, urlsplit
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
def need(name):
value = os.environ.get(name)
if not value:
raise RuntimeError(f"{name} is not set: bind the dependency to this test")
return value
def fleet_url():
# The injected WebDriver URL has no credentials; the fleet id is the user, the token the password.
url = urlsplit(need("MAXOPERF_FLEET_EU_WEBDRIVER_URL"))
fleet_id = url.path.split("/")[2]
return url._replace(netloc=f"{fleet_id}:{quote(need('MAXOPERF_FLEET_EU_TOKEN'), safe='')}@{url.netloc}").geturl()
def test_maxomart_checkout():
request = urllib.request.Request(
f"{need('MAXOPERF_VS_PAYMENTS_URL')}/charges",
data=json.dumps({"amount": 4200, "currency": "usd"}).encode(),
headers={"Authorization": f"Bearer {need('SECRET_PAYMENTS_API_KEY')}", "Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=30) as charge:
assert charge.status == 201
assert json.load(charge)["status"] == "approved"
options = Options()
options.add_argument("--no-sandbox")
driver = webdriver.Remote(command_executor=fleet_url(), options=options)
try:
wait = WebDriverWait(driver, 20)
driver.get(need("MAXOPERF_TUNNEL_SHOP_URL"))
for label in ("Add to cart", "Checkout"):
wait.until(EC.element_to_be_clickable((By.XPATH, f"//button[normalize-space()='{label}']"))).click()
wait.until(lambda d: "Order confirmed" in d.page_source)
finally:
driver.quit()

The script reaches the fleet with Selenium. The injected WebDriver URL carries no credentials, so the script adds the fleet id (the third path segment) as the user name and the token as the password. See Browser fleets.

These are the seven variables the script receives:

  • Secret PAYMENTS_API_KEY: SECRET_PAYMENTS_API_KEY.
  • Virtual service payments, stem PAYMENTS: MAXOPERF_VS_PAYMENTS_URL.
  • Tunnel, stem SHOP: MAXOPERF_TUNNEL_SHOP_URL.
  • Browser fleet eu-browsers, stem EU, four variables:
    • MAXOPERF_FLEET_EU_WEBDRIVER_URL
    • MAXOPERF_FLEET_EU_PLAYWRIGHT_URL
    • MAXOPERF_FLEET_EU_CDP_URL
    • MAXOPERF_FLEET_EU_TOKEN

The REST call that creates each binding:

PUT /v1/tests/{testId}/secret-bindings
PUT /v1/virtual-services/bindings
PUT /v1/tests/{testId}/dependencies/tunnels/{tunnelId}
PUT /v1/tests/{testId}/dependencies/browser-fleets/{fleetId}

The REST bodies are in Automate.

A builder test refers to a dependency differently. Open Payments health, find the request URL and click Use dependency beside it. Pick the payments virtual service. The URL field now holds a chip, and its Env var name is PAYMENTS. The test’s Dependencies tab shows that row as Used in the test model. You never typed ${MAXOPERF_RUNTIME__MAXOPERF_VS_PAYMENTS_URL}. See Automate.

A virtual service chip in the request URL field. The Dependencies tab marks the binding Used in the test model.

Open Maxomart checkout, click Run test, then click Start run in the dialog.

  1. The run page shows a Preparing dependencies banner naming what MaxoPerf is waiting for: payments (deploying) and eu-browsers (starting fleet). The tunnel is already online, so it is ready at once.

  2. When every lease is Ready, the runners start with the seven variables set.

  3. Open the run’s Dependencies tab. It lists one row per dependency:

    • Virtual service payments: Ready, Started by this run, with Open analytics.
    • The tunnel: Ready, no badge, with Open stats.
    • Browser fleet eu-browsers: Ready, Started by this run, with Open sessions.
    • The secret: no state. The row shows the variable name, never the value.
  4. The run ends Passed. The leases keep their last state (Ready), the virtual service and the fleet are stopped, and the tunnel is still online, because a run never stops a tunnel.

The script drives a browser on the fleet, not on the runner. The run’s Video tab therefore shows the runner’s own idle screen next to the list of WebDriver commands the script sent (navigate, find element, click). Its HAR and console are empty. The fleet’s Open sessions page lists one session per iteration, with a step count and the state of its HAR and console capture.

The run's Dependencies tab after a passed run. The secret row shows the variable name only.

Before you rotate the key or stop the payment service, check who else uses it.

  • On Secrets, click Used by on the PAYMENTS_API_KEY row.
  • On the payments virtual service, the tunnel and the eu-browsers fleet, open the Used by section.

Each list names the tests that bind the entity, the variable each one uses, whether the binding was made by hand (manual) or by a builder chip (inline), and any run holding it right now. The same data comes from GET /v1/dependencies/usage. See Run-time behavior.

The Used by list on a virtual service names each test that binds it and how.