# Get started with zuplo test

This guide takes a Zuplo project from no tests to a green test run against a
local dev server, in three steps. It covers where test files live, what
`TestHelper` gives you, and the CLI flags worth knowing. The only install is one
assertion library.

Every test `zuplo test` runs is an integration test: it makes a real HTTP
request to a real running gateway and asserts on the real response. The endpoint
is a command-line flag, so the same files run against local dev, a preview
deployment, or production.

<Stepper>

1. **Install an assertion library**

   `describe`, `it`, the lifecycle hooks, and `TestHelper` come from
   `@zuplo/test`, which arrives transitively with the `zuplo` package. The CLI
   requires Node.js 24.0.0 or later, which every sample here assumes.

   Assertions in these docs use [`expect`](https://www.chaijs.com/api/bdd/) from
   chai. Chai is **not** included in a new Zuplo project, so install it
   yourself:

   ```bash
   npm install --save-dev chai @types/chai
   ```

   The CLI marks `chai` as external when it compiles your tests, so it resolves
   from `node_modules` at run time. Skip the install step and the compiler fails
   with `Cannot find package 'chai'` before any test runs.

   :::tip

   Prefer no dependencies? Node's built-in
   [`node:assert/strict`](https://nodejs.org/api/assert.html#strict-assertion-mode)
   works instead and needs nothing installed. The strict form compares with
   `===`, so `assert.equal(200, "200")` fails instead of quietly passing.

   :::

2. **Write the first test**

   Test files go in a `tests` folder at the root of your project and must end in
   `.test.ts`. Nested folders are fine.

   ```ts title="/tests/health.test.ts"
   import { describe, it, TestHelper } from "@zuplo/test";
   import { expect } from "chai";

   describe("gateway", () => {
     it("serves the root route", async () => {
       const response = await fetch(TestHelper.TEST_URL);
       expect(response.status).to.equal(200);
     });
   });
   ```

   `TestHelper.TEST_URL` is whatever you passed to `--endpoint`. Build request
   URLs from it rather than hard-coding a host — that is the single change that
   lets one suite serve every environment:

   ```ts
   const url = (path: string) => new URL(path, TestHelper.TEST_URL).toString();

   const response = await fetch(url("/v1/orders"));
   ```

3. **Run it**

   Start the dev server in one terminal:

   ```bash
   npx zuplo dev
   ```

   Run the suite in another:

   ```bash
   npx zuplo test --endpoint http://localhost:9000
   ```

   The CLI discovers every `tests/**/*.test.ts` file, compiles them into
   `.zuplo/__tests__`, and runs them. `.zuplo` is generated output — leave it
   out of source control.

   :::tip

   Add the endpoint you use most to a script so the common case is one word:

   ```json title="package.json"
   {
     "scripts": {
       "test": "zuplo test --endpoint http://localhost:9000"
     }
   }
   ```

   :::

</Stepper>

## TestHelper reference

`TestHelper` has two static members.

| Member                   | Type                     | Description                                                                                                        |
| ------------------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------ |
| `TestHelper.TEST_URL`    | `string`                 | The value passed to `--endpoint`. Throws if `zuplo test` was not given one.                                        |
| `TestHelper.environment` | `Record<string, string>` | The test process environment. This is `process.env` — the two are interchangeable, including anything from `.env`. |

`TestHelper.environment` reads the environment of the **test process**, not the
environment variables configured on your Zuplo project. Those belong to the
gateway; these belong to the test process. Fixture tokens, API keys, and seed
data come in this way:

```bash
TENANT_A_JWT=eyJhbGciOi... npx zuplo test --endpoint http://localhost:9000
```

```ts title="/tests/auth.test.ts"
import { describe, it, TestHelper } from "@zuplo/test";
import { expect } from "chai";

describe("auth", () => {
  it("accepts a valid token", async () => {
    const response = await fetch(`${TestHelper.TEST_URL}/v1/orders`, {
      headers: {
        Authorization: `Bearer ${TestHelper.environment.TENANT_A_JWT}`,
      },
    });
    expect(response.status).to.equal(200);
  });
});
```

The CLI also loads a `.env` file from the directory you run it in, so local
fixture values can live there instead of on the command line.

:::warning

Never commit fixture credentials. Keep them in `.env` (gitignored) locally and
in your CI provider's secret store in the pipeline.

:::

## Select which tests run

Two flags select tests by name. Both match against the full test name, which is
the `describe` label and the `it` label joined together, so a test named
`smoke: orders route answers` is selected by `--filter smoke` even when its
enclosing `describe` does not match.

```bash
# Only tests whose name contains "auth"
npx zuplo test --endpoint http://localhost:9000 --filter "auth"

# Regex form — wrap the pattern in slashes
npx zuplo test --endpoint http://localhost:9000 --filter "/#label[Aa]/"

# Everything except tests tagged [slow] in their name
npx zuplo test --endpoint http://localhost:9000 --skip-filter "\[slow\]"
```

`--skip-filter` is applied after `--filter`, so the two compose. This is the
mechanism behind production smoke checks: name the read-only subset
consistently, then run only that subset against production.

## Skip a test in code

Prefix a suite or test with `.skip` (or its alias `.ignore`) to declare it
without running it:

```ts title="/tests/skip.test.ts"
import { describe, it } from "@zuplo/test";
import { expect } from "chai";

describe("arithmetic", () => {
  it.skip("this test is declared but not run", () => {
    expect(1 + 4).to.equal(6);
  });

  it("this test runs", () => {
    expect(1 + 4).to.equal(5);
  });
});
```

:::caution

`.only` takes effect only when the run is started with `--only`. Without the
flag, `zuplo test` runs the marked test **and every other test** — silently the
opposite of what you wanted, and it looks like it worked.

```bash
npx zuplo test --endpoint http://localhost:9000 --only
```

With the flag, only the marked tests and suites execute. If your CLI does not
recognize `--only`, upgrade the `zuplo` package — or use `--filter` to narrow
the run by test name, which works on every version.

:::

## Next steps

- [Gateway test recipes](./testing-recipes.mdx) — what to assert at a gateway,
  as copy-pasteable files
- [Testing preview environments](./testing-preview-environments.mdx) — run the
  same suite against the real deployment of your branch
- [Testing GitHub deployments](./github-deployment-testing.mdx) — make a failing
  gateway test block the merge
- [Testing overview](./testing.mdx) — when to run what, and why
