node-testing

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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "node-testing" with this command: npx skills add ivantorresedge/molcajete.ai/ivantorresedge-molcajete-ai-node-testing

Node.js Testing

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.

Integration Testing

Fastify Inject

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", });

CRUD Test Pattern

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

Validation Error Testing

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.

Test Setup

Database Cleanup

beforeEach(async () => { await prisma.$executeRawTRUNCATE TABLE appointments, users CASCADE; });

afterAll(async () => { await prisma.$disconnect(); });

Test Data Factories

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) }); }

Token Generation

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" });

Authentication Testing

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); }); });

Testcontainers

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(); });

Key Rules

  • Start once per suite — Not per test (too slow)

  • Set generous timeouts — 60s for beforeAll with containers

  • Truncate between tests — TRUNCATE ... CASCADE is fast

  • Use Alpine images — Smaller, faster to pull

  • Use withReuse() — Keeps container between dev test runs

Multi-Container

const [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.

E2E Testing

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"); });

Concurrency Testing

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.

Post-Change Verification

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.

Reference Files

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

react-writing-code

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

typescript-writing-code

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

typescript-testing

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

code-documentation

No summary provided by upstream source.

Repository SourceNeeds Review