Spectest
declarative API testing CLI for fast HTTP endpoint tests
[](https://github.com/justiceo/spectest/actions/workflows/build.yml)
[](https://github.com/justiceo/spectest/actions/workflows/test.yml)
[](https://www.npmjs.com/package/spectest)
[](https://github.com/justiceo/spectest/blob/main/LICENSE)
Spectest is a lightweight API testing CLI for writing declarative HTTP endpoint tests in JavaScript, JSON, or YAML. Use it to verify REST APIs, assert status codes, headers, JSON payloads, and schemas, test multi-step API flows, replay recorded outbound HTTP calls, and run concurrency or load checks without a browser automation framework.
## Features
* **Declarative API tests**: describe requests and expected responses with a simple `*.spectest.*` file.
* **HTTP assertions**: validate status codes, response headers, partial JSON bodies, and Zod or JSON schemas.
* **OpenAPI loading**: run OpenAPI 3.0/3.1 operations directly with `--openapi`.
* **API flow testing**: chain login, setup, teardown, and dependent requests with shared response state.
* **Record and replay**: capture outbound `fetch`, `http`, `https`, and common Node library calls into reusable cassettes.
* **CLI-first workflow**: run tests with `npx spectest`, filter by tags or names, randomize ordering, and snapshot failures.
* **Concurrency and load checks**: send tests in parallel, rate-limit requests, or bombard endpoints to check API robustness.
## Getting Started
A spectest is a collection of test cases. The only required properties of a test case are
* `name` - used to identify the case
* `endpoint` - the API endpoint under test.
Most test cases include a `request` property which mirrors the fetch [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) schema, and a `response` property that is used for assertions. The `response` property also mirrors the fetch [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) schema.
### 1. Define the server environment
At the root of your project, create the file `spectest.config.js`. For the purposes of this guide, we'll be using https://jsonplaceholder.typicode.com, which provides "free fake and reliable API for testing and prototyping".
```js
// spectest.config.js
export default {
baseUrl: 'https://jsonplaceholder.typicode.com',
testDir: './test',
filePattern: '\\.spectest\\.',
};
```
For actual testing, the `baseUrl` should be the hostname of your server like `http://localhost:3000` for Express servers. The CLI would search for and process files that match `filePattern` under the `testDir`. For more config options, see [Config reference](#config-options).
### 2. Create some spec test cases
Create the file `test/jsonpayload.spectest.js` and paste the code below in it. **Note:** the file name needs to match the pattern defined in the config above.
```js
// jsonpayload.spectest.js
const tests = [
// This basic example fetches {baseUrl}/todos/1 and asserts response status is 'OK'.
{
name: "Fetch TODO 1",
endpoint: "/todos/1",
},
// In this case, spectest would assert the actual response matches both status code and json body.
{
name: "Create a post",
endpoint: "/posts",
request: {
method: "POST",
headers: { "Content-Type": "application/json; charset=UTF-8" },
body: { title: "foo", body: "bar", userId: 1 },
},
response: {
status: 201,
json: { id: 101, title: "foo", body: "bar", userId: 1 },
},
},
];
export default tests;
```
A `*.spectest.js` file can have `classes`, `functions`, use `imports` and do anything you would with Javascript. The default export may be either an array of test cases or an object with a `tests` array and optional `name` property. For the full schema of a test case, see [Testcase reference](#test-case-options)
### 3. Run the test cases
You can run the test cases with:
```bash
$ npx spectest
# Run the cases in a specific file
$ npx spectest jsonpayload.spectest.js
# Set/override the configs in step 1
$ npx spectest --base-url="https://jsonplaceholder.typicode.com" jsonpayload.spectest.js
```
All three commands above will have the same output, and it would look like:
```
📊 Test Summary:
[âś…] Fetch TODO 1 (53ms)
[âś…] Create a post (108ms)
âś… 2/2 tests passed!
đź“‹ Server logs captured: 0
⏱️ Latency: min 53ms; avg 80ms; max 108ms
⏱️ Testing time: 0.11s; Total time: 0.18s
```
### Running OpenAPI documents
Spectest can load OpenAPI 3.0 and 3.1 documents directly:
```bash
npx spectest --openapi ./examples/openapi/jsonplaceholder.yaml --base-url=https://jsonplaceholder.typicode.com
```
Generated tests stay in memory; Spectest does not write `.spectest` files for this workflow (see `spectest generate openapi-tests` below if you want a written scaffold instead). Spectest uses examples or schema defaults for required path/query/header/cookie parameters and required JSON request bodies. Operations that need missing values, unsupported media types, unsupported parameter serialization, unresolved external refs, unsupported schema constructs, or unavailable auth/hook lookups are generated as skipped tests with skip reasons.
When both `--openapi` and `testDir` are configured, Spectest loads OpenAPI-generated tests and hand-written suites into the same run and dependency graph — a hand-written suite can `dependsOn` a spec-generated `operationId`, and duplicate `operationId`s across the two sources fail fast via the same uniqueness check.
#### Multiple examples per operation
If a request body, parameter, or response defines an `examples` map (rather than a single `example`), Spectest generates one test per entry. Each generated test's `operationId` becomes `${operationId}+${exampleKey}` and its name gets a ` — ${exampleKey}` suffix.
The expected response for a given request example is resolved in order:
1. `x-spectest.status` declared on that example.
2. A response example under a documented non-2xx status sharing the same key as the request example.
3. The lowest documented `2xx` status (the v1 default).
An operation with no `examples` map behaves exactly as before (a single generated test, unsuffixed `operationId`).
#### `x-spectest` vendor extension
A single vendor extension, allowed on an operation and on an individual entry inside an `examples` map (example-level overrides operation-level):
```yaml
x-spectest:
status: 400
tags: [slow, real-backend]
skip: true
skipReason: "hits real registrar, run manually"
phase: setup | main | teardown
dependsOn: [operationId, "otherOperationId+exampleKey"]
beforeSend: hookName # looked up in cfg.openapiHooks
postTest: hookName
security: none | variantName
generate: # dynamic values, see below
orderId: uuid
"product.domainName": shortId
```
#### Named hook registry
Add `openapiHooks` to `spectest.config.js` to give `x-spectest.beforeSend`/`postTest` something to resolve against:
```js
export default {
openapiHooks: {
extractOneTimeToken: {
postTest: async (res, state, ctx) => { /* scrape ctx.logs, stash in state */ },
},
},
};
```
A name referenced by `x-spectest` that isn't in `openapiHooks` generates a skipped test with a reason, the same way a missing `openapiAuth` hook does — it never crashes the loader.
#### Dynamic example values
Use `{{uuid}}`, `{{timestamp}}`, or `{{shortId}}` as a literal example string to get a fresh value resolved once per generated test (stable across `repeat`/`bombard` reruns of that same test). For object bodies, `x-spectest.generate` is the path-keyed alternative — list which fields need freshness per run instead of rewriting the example. Both are opt-in; Spectest still never synthesizes data for a property that has no example.
#### Explicit non-default auth cases
`openapiAuth` entries can be either a single hook (as in v1) or a named-variant map, to let you deliberately generate a test with missing/expired credentials:
```js
export default {
openapiAuth: {
session: {
valid: async (ctx) => ({ headers: { Cookie: 'session=...' } }),
expired: async (ctx) => ({ headers: { Cookie: 'session=expired' } }),
missing: async () => ({}),
},
},
};
```
Set `x-spectest.security: none` to bypass security application entirely for an example, or `x-spectest.security: expired` (etc.) to pick a variant. With no override, a variant map defaults to its `valid` entry.
#### Native `links` for chaining
Standard OpenAPI `responses.