This skill should be used when writing, reviewing, or debugging Node.js backend tests. It covers integration testing with Fastify inject and Supertest, CRUD test patterns, authentication testing, Testcontainers for Docker-based database testing, and end-to-end user flow testing.
From mnpx claudepluginhub molcajeteai/plugin --plugin mThis skill uses the workspace's default tool permissions.
references/e2e-testing.mdreferences/integration-testing.mdreferences/testcontainers.mdSearches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Executes pre-written implementation plans: critically reviews, follows bite-sized steps exactly, runs verifications, tracks progress with checkpoints, uses git worktrees, stops on blockers.
Quick reference for testing Node.js backend services. Builds on the typescript-testing skill (Vitest config, mocking, coverage) — this skill covers backend-specific patterns. Reference files provide full examples and edge cases.
Test routes without starting an HTTP server — fast, no port conflicts:
const response = await app.inject({
method: "POST",
url: "/appointments",
headers: { authorization: `Bearer ${testToken}` },
payload: {
doctorId: "doctor-123",
dateTime: "2024-06-15T10:00:00Z",
},
});
expect(response.statusCode).toBe(201);
expect(response.json()).toMatchObject({
id: expect.any(String),
status: "scheduled",
});
Every resource endpoint needs tests for:
| Operation | Success | Error Cases |
|---|---|---|
| Create | 201 + resource | 400 (validation), 409 (conflict), 401 (no auth) |
| Read | 200 + resource | 404 (not found), 403 (wrong user) |
| List | 200 + array + pagination | Filter/sort edge cases |
| Update | 200 + updated | 403 (not owner), 404, 400 (validation) |
| Delete | 204 | 403 (not admin), 404 |
it("rejects invalid input with detailed errors", async () => {
const response = await app.inject({
method: "POST",
url: "/users",
payload: { email: "not-an-email", name: "" },
});
expect(response.statusCode).toBe(400);
expect(response.json().details).toHaveProperty("email");
expect(response.json().details).toHaveProperty("name");
});
See references/integration-testing.md for Supertest, full CRUD patterns, response body assertions, and test helpers.
beforeEach(async () => {
await prisma.$executeRaw`TRUNCATE TABLE appointments, users CASCADE`;
});
afterAll(async () => {
await prisma.$disconnect();
});
let counter = 0;
export function createTestUser(overrides = {}): CreateUserInput {
counter++;
return { email: `test-${counter}@example.com`, name: `User ${counter}`, role: "patient", ...overrides };
}
export async function seedUser(overrides = {}) {
return prisma.user.create({ data: createTestUser(overrides) });
}
export function generateTestToken(overrides = {}): string {
return jwt.sign({ sub: "test-user", role: "patient", ...overrides }, process.env.JWT_SECRET!, { expiresIn: "1h" });
}
export const patientToken = generateTestToken({ role: "patient" });
export const doctorToken = generateTestToken({ sub: "doctor-id", role: "doctor" });
export const adminToken = generateTestToken({ sub: "admin-id", role: "admin" });
describe("authorization", () => {
it("allows admin to delete users", async () => {
const user = await seedUser();
const response = await app.inject({
method: "DELETE",
url: `/users/${user.id}`,
headers: { authorization: `Bearer ${adminToken}` },
});
expect(response.statusCode).toBe(204);
});
it("denies patient from deleting users", async () => {
const response = await app.inject({
method: "DELETE",
url: "/users/123",
headers: { authorization: `Bearer ${patientToken}` },
});
expect(response.statusCode).toBe(403);
});
it("returns 401 with expired token", async () => {
const expired = jwt.sign({ sub: "id", role: "patient" }, secret, { expiresIn: "0s" });
const response = await app.inject({
method: "GET",
url: "/users/me",
headers: { authorization: `Bearer ${expired}` },
});
expect(response.statusCode).toBe(401);
});
});
Spin up real Docker containers for tests. No mocking the database.
import { PostgreSqlContainer } from "@testcontainers/postgresql";
let container: StartedPostgreSqlContainer;
beforeAll(async () => {
container = await new PostgreSqlContainer("postgres:16-alpine").start();
process.env.DATABASE_URL = container.getConnectionUri();
execSync("pnpm dlx prisma migrate deploy", { env: process.env });
}, 60_000); // Generous timeout for container startup
afterAll(async () => {
await prisma.$disconnect();
await container.stop();
});
beforeAll with containersTRUNCATE ... CASCADE is fastwithReuse() — Keeps container between dev test runsconst [pgContainer, redisContainer] = await Promise.all([
new PostgreSqlContainer("postgres:16-alpine").start(),
new GenericContainer("redis:7-alpine").withExposedPorts(6379).start(),
]);
See references/testcontainers.md for global setup, container reuse, Redis, multi-container networks, and performance tips.
Test complete user flows through the entire system:
it("completes registration → login → profile access", async () => {
// Register
const reg = await app.inject({
method: "POST", url: "/auth/register",
payload: { email: "new@example.com", password: "pass123!", name: "Test" },
});
expect(reg.statusCode).toBe(201);
// Login
const login = await app.inject({
method: "POST", url: "/auth/login",
payload: { email: "new@example.com", password: "pass123!" },
});
const { accessToken } = login.json();
// Access profile
const profile = await app.inject({
method: "GET", url: "/users/me",
headers: { authorization: `Bearer ${accessToken}` },
});
expect(profile.statusCode).toBe(200);
expect(profile.json().email).toBe("new@example.com");
expect(profile.json()).not.toHaveProperty("passwordHash");
});
it("handles concurrent bookings for same slot", async () => {
const responses = await Promise.all(
Array.from({ length: 5 }, () =>
app.inject({ method: "POST", url: "/appointments", payload: sameSlotData, headers: authHeaders })
)
);
expect(responses.filter((r) => r.statusCode === 201)).toHaveLength(1);
expect(responses.filter((r) => r.statusCode === 409)).toHaveLength(4);
});
See references/e2e-testing.md for complete user flows, multi-service integration, error scenarios, and test organization.
After writing or modifying tests, run the full verification protocol:
pnpm run type-check && pnpm run lint && pnpm run format && pnpm run test
All 4 steps must pass. See typescript-writing-code skill for details.
| File | Description |
|---|---|
| references/integration-testing.md | Fastify inject, Supertest, CRUD patterns, auth testing, test helpers |
| references/testcontainers.md | Docker-based testing, PostgreSQL containers, Redis, multi-container |
| references/e2e-testing.md | Complete user flows, concurrency testing, multi-service integration |