From global-plugin
Use when adding tests, changing testing patterns, or reviewing a PR's test plan. Do NOT use for missing-coverage analysis (use `coverage-gap-detection`) or for risk assessment (use `change-risk-evaluation`). Covers test pyramid, unit vs integration vs e2e split, flake hygiene, test data, mocks vs real services.
npx claudepluginhub lgerard314/global-marketplace --plugin global-pluginThis skill is limited to using the following tools:
Keep the test portfolio useful — fast where it matters, real where correctness requires it, minimal where it doesn't.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Share bugs, ideas, or general feedback.
Keep the test portfolio useful — fast where it matters, real where correctness requires it, minimal where it doesn't.
vi.useFakeTimers(), never rely on real clocks or wall time. — Why: a test that passes at 11:59 PM but fails at midnight is a flake factory. Fake timers let you assert on exact relative durations without sleep.| Thought | Reality |
|---|---|
| "I mocked Prisma, the test is fast" | Repository tests must use real Prisma against a real DB. The mock cannot enforce FK constraints, trigger cascade behavior, or fail on migration drift. |
| "Just snapshot it" | Snapshot tests rot silently. Reviewers rubber-stamp diffs they do not read. Use explicit assertions on the values that actually matter. |
| "Sleep(500) until it passes" | sleep converts a race condition into a flake factory. Use vi.useFakeTimers() or waitFor with an explicit condition instead. |
Bad — the test never touches a real database:
// mocked Prisma — migration state, constraints, and SQL behavior all invisible
vi.mock('@/lib/prisma', () => ({
prisma: {
order: {
findFirst: vi.fn().mockResolvedValue({ id: 'ord-1', status: 'PENDING' }),
},
},
}));
it('returns the pending order', async () => {
const result = await getLatestPendingOrder('cust-1');
expect(result?.status).toBe('PENDING');
});
Good — runs against a real Postgres container:
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import { execSync } from 'node:child_process';
import { PrismaClient } from '@prisma/client';
let container: StartedPostgreSqlContainer;
let prisma: PrismaClient;
beforeAll(async () => {
container = await new PostgreSqlContainer('postgres:16-alpine').start();
process.env.DATABASE_URL = container.getConnectionUri();
execSync('npx prisma migrate deploy', { env: process.env });
prisma = new PrismaClient();
});
afterAll(async () => {
await prisma.$disconnect();
await container.stop();
});
it('returns the latest pending order for a customer', async () => {
const customer = await prisma.customer.create({ data: { email: 'a@test.com' } });
await prisma.order.createMany({
data: [
{ customerId: customer.id, status: 'PAID', createdAt: new Date('2024-01-01') },
{ customerId: customer.id, status: 'PENDING', createdAt: new Date('2024-01-02') },
],
});
const result = await getLatestPendingOrder(customer.id);
expect(result?.status).toBe('PENDING');
expect(result?.createdAt.toISOString()).toBe('2024-01-02T00:00:00.000Z');
});
Bad — fixture breaks when any unrelated field changes:
// fixtures/order.json
{
"id": "ord-1",
"customerId": "cust-1",
"status": "PENDING",
"total": 99.99,
"currency": "USD",
"shippingAddressId": "addr-1",
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
}
// test
const order = JSON.parse(readFileSync('fixtures/order.json', 'utf-8'));
it('calculates tax', () => {
expect(calculateTax(order)).toBe(9.00);
});
Good — factory provides valid defaults; test overrides only what matters:
import { Factory } from 'fishery';
import type { Order } from '@prisma/client';
const orderFactory = Factory.define<Order>(() => ({
id: `ord-${Math.random().toString(36).slice(2)}`,
customerId: 'cust-default',
status: 'PENDING',
total: 100,
currency: 'USD',
shippingAddressId: 'addr-default',
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
}));
it('calculates 9% tax on a USD order', () => {
const order = orderFactory.build({ total: 100, currency: 'USD' });
expect(calculateTax(order)).toBe(9);
});
it('calculates 25% VAT on a EUR order', () => {
const order = orderFactory.build({ total: 100, currency: 'EUR' });
expect(calculateTax(order)).toBe(25);
});
vi.useFakeTimers() with advance vs sleepBad — sleep introduces wall-time dependency and slows the suite:
it('expires a session after 15 minutes', async () => {
const session = createSession();
await new Promise((r) => setTimeout(r, 900_000)); // 15 min — nobody actually waits this
expect(isExpired(session)).toBe(true);
});
Good — fake timers advance instantly with no real wall time:
it('expires a session after 15 minutes', () => {
vi.useFakeTimers();
const session = createSession();
vi.advanceTimersByTime(14 * 60 * 1000); // 14 min
expect(isExpired(session)).toBe(false);
vi.advanceTimersByTime(60 * 1000); // +1 min = 15 min total
expect(isExpired(session)).toBe(true);
vi.useRealTimers();
});
A healthy backend service in this codebase should aim for roughly 70 % unit / 25 % integration / 5 % e2e by test count.
| Pattern | Symptom | Fix |
|---|---|---|
| Too many unit tests | Refactors break tests; coverage of integration paths is thin | Replace with integration tests at trust boundaries |
| Too many integration tests | Slow CI; flake | Move pure-logic checks to unit |
| Too many e2e tests | Long feedback loop; brittle to UI churn | Cap to critical user flows |
Concretely, for a NestJS service with Prisma:
*.spec.ts files co-located with the module. They import the class under test directly, inject stub dependencies, and complete in under 50 ms each.*.integration.spec.ts or a dedicated test/integration/ directory. They spin up a Testcontainers Postgres instance, run migrations, and exercise the full NestJS application with supertest or NestJS's Test.createTestingModule.test/e2e/ and run against a fully deployed instance (staging, or a local docker-compose stack). They use Playwright and cover 5–10 critical journeys maximum.Use @testcontainers/postgresql (from the testcontainers monorepo).
Per-test isolation: wrap each test in a transaction that rolls back.
import { PrismaClient } from '@prisma/client';
let prisma: PrismaClient;
// established in beforeAll (container already started)
beforeEach(async () => {
await prisma.$executeRaw`BEGIN`;
});
afterEach(async () => {
await prisma.$executeRaw`ROLLBACK`;
});
Provide the same PrismaClient to the NestJS test module so both share the transaction:
const module = await Test.createTestingModule({
providers: [
OrderService,
{ provide: PrismaService, useValue: prisma }, // shared instance
],
}).compile();
Set DATABASE_URL in Vitest globalSetup before any test module imports Prisma:
// vitest.global-setup.ts
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { execSync } from 'node:child_process';
export default async function setup() {
const container = await new PostgreSqlContainer('postgres:16-alpine').start();
process.env.DATABASE_URL = container.getConnectionUri();
execSync('npx prisma migrate deploy', { env: process.env });
return async () => container.stop();
}
Then in vitest.config.ts:
export default defineConfig({
test: {
globalSetup: ['./vitest.global-setup.ts'],
},
});
Use Fishery (fishery on npm) for structured factory definitions.
// test/factories/customer.factory.ts
import { Factory } from 'fishery';
import type { Customer } from '@prisma/client';
export const customerFactory = Factory.define<Customer>(({ sequence }) => ({
id: `cust-${sequence}`,
email: `customer-${sequence}@example.com`,
name: 'Test Customer',
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
}));
// test/factories/order.factory.ts
import { Factory } from 'fishery';
import type { Order } from '@prisma/client';
export const orderFactory = Factory.define<Order>(({ sequence }) => ({
id: `ord-${sequence}`,
customerId: 'cust-default',
status: 'PENDING',
total: 100,
currency: 'USD',
shippingAddressId: null,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
}));
For integration tests that need DB rows, add a create helper that calls Prisma:
// test/factories/order.factory.ts (extended)
import { PrismaClient } from '@prisma/client';
export function makeOrderFactory(prisma: PrismaClient) {
return {
create: (overrides: Partial<Order> = {}) =>
prisma.order.create({ data: { ...orderFactory.build(), ...overrides } }),
};
}
Keep factories honest: if a default value would violate a DB constraint, fix the factory default rather than adding try/catch in the test.
Avoid global fixtures (JSON, SQL seeds) — they couple tests and create order-dependence; only OK for never-changing reference data.
| Cause | Symptom | Fix |
|---|---|---|
| Timing | Intermittent timeouts; speed-dependent | Replace setTimeout with vi.useFakeTimers() or condition-based waits |
| Shared state | Order-dependent failures | Reset state in beforeEach; transaction-rollback |
| Non-determinism | Date.now(), Math.random(), network | Inject seeds; mock at boundary |
Seed Math.random if you need controlled randomness:
import seedrandom from 'seedrandom';
const rng = seedrandom('fixed-seed-for-tests');
Quarantine protocol: when a flake is identified, open a ticket, mark the test with it.skip (with a comment linking the ticket), and commit. Do not re-run CI repeatedly trying to get the test to pass.
Detection tooling: Vitest's --reporter=verbose and --retry=2 flag (used sparingly) can surface flakes in CI.
Mock when:
Keep real when:
Test.createTestingModule with real providers; only override the outermost external dependencies.supertest against a real NestJS application instance rather than calling controller methods directly.The concrete boundary for this codebase:
| Layer | What to keep real | What to mock |
|---|---|---|
| Unit test (service logic) | Nothing external | PrismaService, external HTTP clients, email, queues |
| Integration test (repository / service) | Postgres (Testcontainers), NestJS DI, Prisma | Stripe, SendGrid, S3, any external API |
| E2E test (Playwright) | Full stack (app + real Postgres) | Nothing — use a dedicated test environment |
Prefer explicit vi.fn().mockResolvedValue(...) over vi.mock(...) auto-mocks (auto-mocks silently return undefined).
coverage-gap-detection for identifying which paths need tests; change-risk-evaluation for assessing blast radius of a change.prisma-data-access-guard's migration testing concerns — this skill focuses on how to structure the test, not on Prisma query correctness.Produce a markdown report with these sections:
vi.useFakeTimers() + advanceTimersByTime) and deterministic clocks over setTimeout/sleep-based waits for flake hygiene.vi.useFakeTimers() — no sleep, no Date.now() without freezingRequired explicit scans:
vi.mock of a Prisma service or team-owned service with file and line number.toMatchSnapshot, toMatchInlineSnapshot) in non-UI code.setTimeout / sleep call inside a test body.