Supertest api
QA Skills Directory QA Skills is a curated directory of testing-specific skills for AI coding agents (Claude Code, Cursor, Copilot, etc.).
npx -y skills add PramodDutta/qaskills --skill supertest-apiAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
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.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
8.9 KB, ~2.0k tokens by cl100k_base, as published. Nobody here has run it
SuperTest API Testing
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.
Core Principles
- Test the app object, not a running server.
request(app)binds to an ephemeral port per request and tears it down — noapp.listen(), no port conflicts, no orphaned servers in CI. - Export
appseparately from the listener. The single biggest enabler:app.tsexports the Express app,server.tscallslisten(). Tests importapp.tsonly. - Always
await(or return) the request chain. A SuperTest call is a thenable; forgettingawaitmeans 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 inexpect(res.body).toMatchObject(...)where failure diffs are readable.- Real database or none — never half-mocked. Either run integration tests against a disposable database (Testcontainers, SQLite in-memory) or mock the data layer entirely. Mocking two of five queries gives you tests that lie.
- Each test owns its data. Create the records a test needs inside the test (or a
beforeEach), and make cleanup idempotent. Order-dependent suites rot within a sprint.
Setup
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' });
});
});
Patterns
CRUD Round-Trip with Body Assertions
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' }),
);
});
});
Authenticated Requests
// 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 Params, File Upload, and Custom Assertions
// 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');
}
});
Cookies and Sessions
// 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);
Best Practices
- Name tests by behavior and status:
'returns 409 when email already exists', not'test create user 2'. - Cover the unhappy paths the framework will not: malformed JSON body, missing auth, wrong content-type, oversized payload, nonexistent IDs (404 vs 400 for invalid format).
- Run integration tests serially against a shared DB (
jest --runInBand) or give each worker its own schema; parallel workers on one mutable DB produce heisenbugs. - Keep
Content-Typeassertions as regex (/json/) — servers append; charset=utf-8. - Add a
jest.setup.tsthat fails tests on unhandled promise rejections; SuperTest chains silently swallow them otherwise. - For Fastify, call
await app.ready()before passingapp.servertorequest().
Anti-Patterns
app.listen()in test setup. Port collisions across Jest workers, orphan servers on failure.request(app)exists precisely so you never listen.- Forgetting
awaiton the chain. The test exits green while the request is in flight. Enable@typescript-eslint/no-floating-promisesto make this a lint error. - Asserting entire bodies with
toEqualincluding timestamps and IDs. UsetoMatchObjectplusexpect.any(String)for generated fields; full-body equality breaks on every schema addition. - One mega-test that exercises login, create, update, and delete. When it fails at step 14 you debug all 14 steps. Split per behavior, share setup via helpers.
- Seeding through raw SQL while testing through HTTP. Your seed bypasses validation and hashing; create test data through the API or through the same repository layer the app uses.
- Testing third-party middleware (body-parser limits, cors echo) — pin your config in one test if you must, but do not re-test Express itself.
When to Trigger This Skill
supertestis indevDependencies, or the user asks to test Express/Koa/NestJS HTTP endpoints.- An API has no integration tests and the user wants coverage without deploying or binding ports.
- Reviewing failing or flaky API tests that use
app.listen, missingawait, or shared mutable test data. - The user asks how to test auth-protected routes, file uploads, or cookie sessions in Node.
- Setting up the app/server split so an existing Express codebase becomes testable.