seed-skills/supertest-api/SKILL.md
Test Node.js HTTP APIs in-process with SuperTest — request(app) without binding a port, chained .expect assertions, auth headers, JSON body validation, and Jest integration with proper async/await patterns.
npx skillsauth add PramodDutta/qaskills SuperTest API TestingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill makes an AI agent write integration tests for Express/Koa/Fastify-compatible Node HTTP apps using SuperTest: pass the app object directly to request() so no port is bound, chain .expect() for status/header checks, and assert response bodies with Jest matchers. Trigger it when a Node project exposes an Express app, when the user asks to test REST endpoints without spinning up a server, or when supertest is already in devDependencies.
request(app) binds to an ephemeral port per request and tears it down — no app.listen(), no port conflicts, no orphaned servers in CI.app separately from the listener. The single biggest enabler: app.ts exports the Express app, server.ts calls listen(). Tests import app.ts only.await (or return) the request chain. A SuperTest call is a thenable; forgetting await means the test passes before the request even fires..expect(status) for transport, Jest matchers for payload. Status codes and content-type belong in the chain; body shape belongs in expect(res.body).toMatchObject(...) where failure diffs are readable.beforeEach), and make cleanup idempotent. Order-dependent suites rot within a sprint.npm install --save-dev supertest @types/supertest jest ts-jest @types/jest
The app/server split that makes everything testable:
// src/app.ts
import express from 'express';
import { usersRouter } from './routes/users';
export function createApp(): express.Express {
const app = express();
app.use(express.json());
app.use('/api/users', usersRouter);
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
return app;
}
// src/server.ts — the ONLY file that listens; never imported by tests
import { createApp } from './app';
const port = Number(process.env.PORT ?? 3000);
createApp().listen(port, () => console.log(`listening on :${port}`));
First test:
// src/app.test.ts
import request from 'supertest';
import { createApp } from './app';
const app = createApp();
describe('GET /health', () => {
it('responds 200 with status ok', async () => {
const res = await request(app)
.get('/health')
.expect('Content-Type', /json/)
.expect(200);
expect(res.body).toEqual({ status: 'ok' });
});
});
import request from 'supertest';
import { createApp } from './app';
import { resetDb } from '../test/helpers/db';
const app = createApp();
beforeEach(async () => {
await resetDb();
});
describe('POST /api/users', () => {
it('creates a user and returns 201 with the persisted record', async () => {
const res = await request(app)
.post('/api/users')
.send({ email: '[email protected]', name: 'Mira' })
.expect(201);
expect(res.body).toMatchObject({
email: '[email protected]',
name: 'Mira',
});
expect(res.body.id).toEqual(expect.any(String));
// Round-trip: the created resource is retrievable
const fetched = await request(app).get(`/api/users/${res.body.id}`).expect(200);
expect(fetched.body.email).toBe('[email protected]');
});
it('rejects an invalid email with 400 and a field-level error', async () => {
const res = await request(app)
.post('/api/users')
.send({ email: 'not-an-email', name: 'Mira' })
.expect(400);
expect(res.body.errors).toContainEqual(
expect.objectContaining({ field: 'email' }),
);
});
});
// test/helpers/auth.ts — log in once per suite, reuse the token
import request from 'supertest';
import type { Express } from 'express';
export async function getAuthToken(app: Express): Promise<string> {
const res = await request(app)
.post('/api/auth/login')
.send({ email: '[email protected]', password: 'test-password-123' })
.expect(200);
return res.body.token as string;
}
import request from 'supertest';
import { createApp } from './app';
import { getAuthToken } from '../test/helpers/auth';
const app = createApp();
let token: string;
beforeAll(async () => {
token = await getAuthToken(app);
});
describe('DELETE /api/users/:id', () => {
it('returns 401 without a token', async () => {
await request(app).delete('/api/users/u_123').expect(401);
});
it('deletes with a valid bearer token', async () => {
await request(app)
.delete('/api/users/u_123')
.set('Authorization', `Bearer ${token}`)
.expect(204);
});
});
// Query strings via .query() — never hand-concatenate
const res = await request(app)
.get('/api/users')
.query({ page: 2, limit: 10, sort: 'createdAt' })
.expect(200);
expect(res.body.items).toHaveLength(10);
expect(res.body.page).toBe(2);
// multipart upload
await request(app)
.post('/api/avatars')
.set('Authorization', `Bearer ${token}`)
.attach('avatar', 'test/fixtures/avatar.png')
.field('alt', 'profile picture')
.expect(201);
// Function form of .expect() for response-wide invariants
await request(app)
.get('/api/users')
.expect(200)
.expect((response) => {
if (response.body.items.some((u: { password?: string }) => u.password)) {
throw new Error('password leaked in list endpoint');
}
});
// Persist cookies across requests with an agent
const agent = request.agent(app);
await agent
.post('/api/auth/login')
.send({ email: '[email protected]', password: 'test-password-123' })
.expect(200);
// agent carries the session cookie automatically
await agent.get('/api/me').expect(200);
'returns 409 when email already exists', not 'test create user 2'.jest --runInBand) or give each worker its own schema; parallel workers on one mutable DB produce heisenbugs.Content-Type assertions as regex (/json/) — servers append ; charset=utf-8.jest.setup.ts that fails tests on unhandled promise rejections; SuperTest chains silently swallow them otherwise.await app.ready() before passing app.server to request().app.listen() in test setup. Port collisions across Jest workers, orphan servers on failure. request(app) exists precisely so you never listen.await on the chain. The test exits green while the request is in flight. Enable @typescript-eslint/no-floating-promises to make this a lint error.toEqual including timestamps and IDs. Use toMatchObject plus expect.any(String) for generated fields; full-body equality breaks on every schema addition.supertest is in devDependencies, or the user asks to test Express/Koa/NestJS HTTP endpoints.app.listen, missing await, or shared mutable test data.development
Generate test data from the schemas you already have. Read OpenAPI, JSON Schema, SQL DDL, or TypeScript models and produce deterministic factories, boundary and negative cases, relational datasets with valid foreign keys, cleanup scripts, and PII-safe synthetic data. Production records never leave the machine.
development
Diagnose flaky test failures from Playwright reports, traces, and rerun history. Classify each failure as product, test, environment, data, or unknown with cited evidence and a proposed fix. Never auto-modifies code without opt-in.
tools
Test LLM, RAG, MCP, and agentic systems end to end. Build golden datasets, run deterministic checks and LLM judges, score retrieval, probe prompt injection, verify tool use, and gate CI on thresholds. Orchestrates DeepEval, Ragas, promptfoo, and Langfuse.
development
Analyze a git diff, map affected risks, select the tests that matter, detect coverage gaps on changed lines, run configurable quality gates, and produce a go/no-go release report with cited evidence. Recommends only; never merges or deploys.