ZuploZuplo
LoginSign Up
  • Documentation
  • API Reference
Introduction
Getting Started
    Develop using the Portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingMCP - Quick start
    Develop Locally
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth
Concepts
Development
    CORSEnvironment VariablesBranch-Based DeploymentsTestingTroubleshootingGitOps vs TerraformCustom Code
    Local Development
    Guides
Policies
Handlers
API Keys
MCP Server
MCP Gateway
AI Gateway
Developer Portal
Monetization
Deploying & Source Control
Observability
Networking & Infrastructure
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsSecuritySupportTrust & ComplianceChangelog
powered by Zudoku
Development

Testing Your API

Zuplo provides multiple ways to test your API gateway at every stage of development. Whether you are iterating locally, reviewing a pull request in a preview environment, or gating production deployments in CI/CD, the zuplo test command and the @zuplo/test library give you a consistent testing experience.

Testing strategies overview

StrategyWhen to useEndpoint target
Local testingFast feedback while developinghttp://localhost:9000
Preview environmentsValidate changes on a real deployment before merginghttps://<branch>-<id>.zuplo.app
CI/CD integrationAutomated gate that blocks broken changes from reaching productionDeployment URL from your CI provider

All three strategies use the same test files and the same zuplo test command. The only thing that changes is the --endpoint value.

Local testing

Running tests against a local development server gives the fastest feedback loop. Start the server with zuplo dev, then run your test suite against it.

Starting the local server

TerminalCode
npx zuplo dev

The API gateway starts on http://localhost:9000 by default. You can change the port with the --port flag. See the zuplo dev reference for all available options.

Running tests locally

With the dev server running, open a second terminal and run:

TerminalCode
npx zuplo test --endpoint http://localhost:9000

The command discovers every *.test.ts file under the tests/ folder and executes them against the provided endpoint.

You can filter which tests run with the --filter flag. For example, npx zuplo test --endpoint http://localhost:9000 --filter 'auth' runs only tests whose name contains "auth".

Testing with Zuplo services locally

Some features, such as API key authentication and rate limiting, require a connection to Zuplo cloud services. To use these features in local development, link your local project to an existing Zuplo project with zuplo link:

TerminalCode
npx zuplo link

Follow the prompts to select your account, project, and environment. This creates a .env.zuplo file that the dev server reads automatically. For local development, selecting the development environment is recommended.

The .env.zuplo file can contain sensitive information. Add it to your .gitignore file so it is not committed to source control.

Once linked, services like the API Key Authentication policy work locally using the same API key bucket as the linked environment. In the Zuplo Portal, open the Services tab in your project and select API Key Service to create API key consumers, then call your local gateway with the generated key:

TerminalCode
curl http://localhost:9000/your-route \ -H "Authorization: Bearer YOUR_API_KEY"

For more details see Connecting to Zuplo Services Locally.

Setting environment variables locally

Your local dev server does not have access to the environment variables configured in the Zuplo Portal. Instead, create a .env file in your project root:

.env
MY_BACKEND_URL=https://api.example.com MY_SECRET=supersecret

The Zuplo CLI loads these variables automatically when you run npx zuplo dev. See Configuring Environment Variables Locally for more information.

Preview environments

Every branch pushed to your connected source control provider can create an isolated preview environment. Preview environments are full Zuplo deployments that behave the same as production, making them ideal for testing pull requests before merging.

Running tests against a preview environment

Pass the preview environment URL as the endpoint:

TerminalCode
npx zuplo test --endpoint https://your-branch-abc123.zuplo.app

Because preview environments run the full Zuplo runtime, including edge deployment, policies, and connected services, tests that pass here give high confidence that the changes work in production.

Combining local and remote testing

For maximum coverage, test locally first for fast iteration, then run the same test suite against the deployed preview environment:

  1. Start local development and run tests against http://localhost:9000.
  2. Push your branch. Zuplo deploys a preview environment automatically.
  3. Run npx zuplo test --endpoint <preview-url> to verify behavior on the real edge deployment.

CI/CD integration testing

Automated tests in your CI/CD pipeline ensure that every deployment is validated before changes reach production. The zuplo test command works with any CI system.

The examples below use the Zuplo GitHub integration. If you prefer setting up your own CI/CD for more fine-grained control, see Custom CI/CD.

Testing after deployment with GitHub Actions

Using the Zuplo GitHub integration, tests can run after a deployment and block pull requests from being merged. The Zuplo Git Integration sets Deployments and Deployment Statuses for any push to a GitHub branch.

Here is a simple GitHub Action that uses the Zuplo CLI to run the tests after the deployment is successful. Notice how the property github.event.deployment_status.environment_url is set to the API_URL environment variable. This is one way you can pass the URL where the preview environment is deployed into your tests.

/.github/workflows/main.yaml
name: Main on: [deployment_status] jobs: test: name: Test API Gateway runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Use Node.js uses: actions/setup-node@v4 with: node-version-file: ".nvmrc" - name: Run Tests # Useful properties 'environment', 'state', and 'environment_url' run: API_URL=${{ toJson(github.event.deployment_status.environment_url) }} npx zuplo test --endpoint $API_URL

Requiring status checks

GitHub Branch protection can be set in order to enforce policies on when a Pull Request can be merged. The example below sets the "Zuplo Deployment" and "Test API Gateway" as required status that must pass.

Require status checks

When a developer tries to merge their pull request, they will see that the tests haven't passed and the pull request can't be merged.

Test failure

Local testing in CI

You can also run tests against a local Zuplo server inside your CI pipeline before deploying anywhere. This catches issues earlier and avoids deploying broken changes.

/.github/workflows/local-test-then-deploy.yaml
name: Local Test Then Deploy on: push: branches: - main pull_request: jobs: local-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm install - name: Start local server and run tests run: | npx zuplo dev & DEV_PID=$! echo "Waiting for local server to start..." sleep 10 npx zuplo test --endpoint http://localhost:9000 kill $DEV_PID deploy: needs: local-test runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/main' env: ZUPLO_API_KEY: ${{ secrets.ZUPLO_API_KEY }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm install - name: Deploy to Zuplo run: npx zuplo deploy --api-key "$ZUPLO_API_KEY"

For CI/CD examples with other providers, see Custom GitHub Actions, GitLab CI/CD, and CircleCI.

Writing tests

Using Node.js 18 and the Zuplo CLI, it's very easy to write tests that make requests to your API using fetch and then validate expectations with expect from chai.

/tests/my-test.test.ts
import { describe, it, TestHelper } from "@zuplo/test"; import { expect } from "chai"; describe("API", () => { it("should have a body", async () => { const response = await fetch(TestHelper.TEST_URL); const result = await response.text(); expect(result).to.equal(JSON.stringify("What zup?")); }); });

Check out our other sample tests to find one that matches your use-case.

Your test files need to be under the tests folder and end with .test.ts to be picked up by the Zuplo CLI.

Tips for writing tests

This section highlights some of the features of the Zuplo CLI that can help you write and structure your tests. Check out our other sample tests to find one that matches your use-case.

Ignoring tests

You can use .ignore and .only to ignore or run only specific test. The full example is at ignore-only.test.ts

/tests/ignore-only.test.ts
import { describe, it } from "@zuplo/test"; import { expect } from "chai"; /** * This example how to use ignore and only. */ describe("Ignore and only test example", () => { it.ignore("This is a failing test but it's been ignored", () => { expect(1 + 4).to.equals(6); }); // it.only("This is the only test that would run if it weren't commented out", () => { // expect(1 + 4).to.equals(5); // }); });

Filtering tests

You can use the CLI to filter tests by name or regex. The full example is at filter.test.ts

/tests/filter.test.ts
import { describe, it } from "@zuplo/test"; import { expect } from "chai"; /** * This example shows how to filter the test by the name in the describe() function. * You can run `zuplo test --filter '#labelA'` * If you want to use regex, you can do `zuplo test --filter '/#label[Aa]/'` */ describe("[#labelA #labelB] Addition", () => { it("should add positive numbers", () => { expect(1 + 4).to.equals(5); }); });

Using environment variables in tests

You can pass environment variables to your tests by setting them before the zuplo test command. Inside your test files, access them with process.env:

TerminalCode
MY_API_KEY=zpka_abc123 npx zuplo test --endpoint http://localhost:9000
/tests/auth.test.ts
import { describe, it, TestHelper } from "@zuplo/test"; import { expect } from "chai"; describe("Authentication", () => { it("should reject requests without an API key", async () => { const response = await fetch(`${TestHelper.TEST_URL}/my-route`); expect(response.status).to.equal(401); }); it("should accept requests with a valid API key", async () => { const response = await fetch(`${TestHelper.TEST_URL}/my-route`, { headers: { Authorization: `Bearer ${process.env.MY_API_KEY}`, }, }); expect(response.status).to.equal(200); }); });

Writing integration tests

Integration tests verify that your API gateway behaves correctly end-to-end, including routing, policies, and backend connectivity. Because zuplo test runs against a live endpoint (local or deployed), every test is inherently an integration test.

Testing response status and headers

/tests/headers.test.ts
import { describe, it, TestHelper } from "@zuplo/test"; import { expect } from "chai"; describe("Response headers", () => { it("should include CORS headers", async () => { const response = await fetch(`${TestHelper.TEST_URL}/my-route`, { method: "OPTIONS", headers: { Origin: "https://example.com", "Access-Control-Request-Method": "GET", }, }); expect(response.headers.get("access-control-allow-origin")).to.exist; }); it("should return JSON content type", async () => { const response = await fetch(`${TestHelper.TEST_URL}/my-route`); expect(response.headers.get("content-type")).to.include("application/json"); }); });

Testing rate limiting

/tests/rate-limit.test.ts
import { describe, it, TestHelper } from "@zuplo/test"; import { expect } from "chai"; describe("Rate limiting", () => { it("should include rate limit headers", async () => { const response = await fetch(`${TestHelper.TEST_URL}/my-route`, { headers: { Authorization: `Bearer ${process.env.MY_API_KEY}`, }, }); expect(response.headers.get("ratelimit-limit")).to.exist; expect(response.headers.get("ratelimit-remaining")).to.exist; }); });

Testing request validation

/tests/validation.test.ts
import { describe, it, TestHelper } from "@zuplo/test"; import { expect } from "chai"; describe("Request validation", () => { it("should reject invalid request bodies", async () => { const response = await fetch(`${TestHelper.TEST_URL}/my-route`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ invalid: "data" }), }); expect(response.status).to.equal(400); }); it("should accept valid request bodies", async () => { const response = await fetch(`${TestHelper.TEST_URL}/my-route`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "Test", email: "test@example.com" }), }); expect(response.status).to.equal(200); }); });

Unit Tests & Mocking

Advanced

Custom testing can be complicated and is best used only to test your own logic rather than trying to mock large portions of your API Gateway.

It's usually possible to use test frameworks like Mocha and mocking tools like Sinon to unit tests handlers, policies, or other modules. To see an example of how that works see this sample on GitHub: https://github.com/zuplo/zuplo/tree/main/examples/test-mocks

Do note though that not everything in the Zuplo runtime can be mocked. Additionally, internal implementation changes might cause mocking behavior to change or break without notice. Unlike our public API we don't guarantee that mocking will remain stable between versions.

Generally speaking, if you must write unit tests, it's best to test your logic separately from the Zuplo runtime. For example, write modules and functions that take all the arguments as input and return a result, but don't depend on any Zuplo runtime code.

For example, if you have a function that uses an environment variable and want to unit test it.

Don't do this:

Code
import { environment } from "@zuplo/runtime"; export function myFunction() { const myVar = environment.MY_ENV_VAR; return `Hello ${myVar}`; }

Instead do this:

Code
export function myFunction(myVar: string) { return `Hello ${myVar}`; }

Then write your test like this:

Code
import { myFunction } from "./myFunction"; describe("myFunction", () => { it("should return Hello World", () => { expect(myFunction("World")).to.equal("Hello World"); }); });

Polyfills

If you are running unit tests in a Node.js environment, you may need to polyfill some globals. Zuplo itself doesn't run on Node.js, but because Zuplo is built on standard API, testing in Node.js is possible.

If you are running on Node.js 20 or later, you can use the webcrypto module to polyfill the crypto global. You must register this polyfill before any Zuplo code runs.

Code
import { webcrypto } from "node:crypto"; if (typeof crypto === "undefined") { globalThis.crypto = webcrypto; }
Edit this page
Last modified on May 10, 2026
Branch-Based DeploymentsTroubleshooting
On this page
  • Testing strategies overview
  • Local testing
    • Starting the local server
    • Running tests locally
    • Testing with Zuplo services locally
    • Setting environment variables locally
  • Preview environments
    • Running tests against a preview environment
    • Combining local and remote testing
  • CI/CD integration testing
    • Testing after deployment with GitHub Actions
    • Requiring status checks
    • Local testing in CI
  • Writing tests
  • Tips for writing tests
    • Ignoring tests
    • Filtering tests
    • Using environment variables in tests
  • Writing integration tests
    • Testing response status and headers
    • Testing rate limiting
    • Testing request validation
  • Unit Tests & Mocking
    • Polyfills
YAML
YAML
Javascript
Javascript
Javascript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
Javascript