node-testing

SKILL.md

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.$executeRaw`TRUNCATE 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 testsTRUNCATE ... 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
Weekly Installs
3
GitHub Stars
2
First Seen
4 days ago
Installed on
opencode3
gemini-cli3
github-copilot3
codex3
amp3
cline3