Skip to main content

testing-real-service-e2e-no-mocks

Build mock-free integration and E2E tests that hit real databases, real APIs, and real services with structured logging. Use when: replacing mocked tests with real-DB tests, transaction rollback isolation, test data factories, structured JSON-line test logging, webhook simulation, or billing flow verification. Covers Vitest, Supabase, Stripe test mode, and testcontainers.

Source facts

Repository
lev-os/agents
Last source activity
November 2, 2026 at 03:28
Detected SKILL.md language
English
Stars
26
Forks
3

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
7 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
testing-real-service-e2e-no-mocks
description
Build mock-free integration and E2E tests that hit real databases, real APIs, and real services with structured logging. Use when: replacing mocked tests with real-DB tests, transaction rollback isolation, test data factories, structured JSON-line test logging, webhook simulation, or billing flow verification. Covers Vitest, Supabase, Stripe test mode, and testcontainers.
metadata
{"filePattern":["**/integration/**","**/e2e/**","**/test-db*","**/test*factory*","**/test*harness*"],"bashPattern":["\\b(REAL_API_TESTS|integration.test|test.db|transaction.rollback|test.factory)\\b"],"priority":60}
# Perfect E2E & Integration Tests (No Mocks) > **The One Rule:** If a mock hides a bug that would break production, > the mock is worse than no test at all. Test the real thing. > **The Case Against Mocks:** Every mock is a lie about how the system works. > Mocks pass when reality fails. The more critical the path (billing, auth, > data deletion), the more dangerous the mock. Test real databases, real APIs, > real webhooks, real payment providers in test mode. ## The Loop (Mandatory) ``` 1. IDENTIFY → Which mocked test hides the most production risk? 2. PROVISION → Set up real test infrastructure (DB, API keys, webhooks) 3. ISOLATE → Transaction rollback per test (no shared state, no cleanup) 4. FACTORY → Build test data factories (realistic, not minimal) 5. LOG → Structured JSON-line logging: phase, timing, DB snapshots 6. VERIFY → Assert against real responses, not mocked return values 7. GUARD → Production URL blocklist — never hit prod from tests 8. REGRESSION → Every production bug → mock-free regression test ``` ## Mock Risk Assessment Matrix Before writing a test, score how dangerous mocking is: | Code Path | Production Impact | Mock Divergence Risk | Last Bug from Mock | Score | |-----------|:-----------------:|:--------------------:|:------------------:|-------| | *function* | 1-5 (revenue/data) | 1-5 (how often mock lies) | date or N/A | Impact × Risk | **Rule:** Score ≥ 8 = MUST be mock-free. Score ≥ 4 = SHOULD be mock-free. Score < 4 = mock is acceptable (low-risk helper functions). --- ## The Anti-Mock Manifesto ### Why Mocks Lie | Mock Pattern | The Lie | The Production Bug It Hides | |-------------|---------|----------------------------| | `vi.mock("@/lib/db/client")` | DB always responds instantly | Connection pool exhaustion, query timeout | | `mockStripe.subscriptions.cancel.mockResolvedValue({})` | Stripe always succeeds | Rate limiting, network errors, invalid subscription state | | `vi.mock("@/lib/email/sender")` | Emails always send | Template rendering errors, DNS failures, rate limits | | `mockDb.query.subscriptions.findMany.mockResolvedValue([{status: "active"}])` | Data shape matches reality | Schema drift, missing columns, wrong types | | `vi.mock("@/lib/services/subscription")` | Business logic works | Cascade deletion, grace period edge cases | ### The Fix: Test Pyramid Without Mocks ``` ┌──────────┐ │ E2E │ Real browser, real API, real payment provider │ (few) │ Stripe test mode, PayPal sandbox ├──────────┤ │ Integr. │ Real DB (transaction rollback), real services │ (many) │ Factory-seeded data, structured logging ├──────────┤ │ Unit │ Pure functions ONLY — no I/O, no DB, no network │ (most) │ Mocks acceptable ONLY for pure function deps └──────────┘ ``` **Critical insight:** Most projects have an inverted pyramid (many unit tests with mocks, few integration tests). Flip it for critical paths. --- ## Pattern 1: Transaction Rollback Isolation The foundation pattern. Every test runs in a transaction that's rolled back. Zero cleanup needed. Zero shared state. ```typescript // tests/utils/test-db.ts import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { beforeAll, beforeEach, afterEach, afterAll } from "vitest"; let testDb: PostgresJsDatabase; let testSql: postgres.Sql; export function withTestTransaction(options?: { suiteName?: string }) { beforeAll(async () => { testSql = postgres(TEST_DATABASE_URL, { prepare: false, max: 1, // Single connection for transaction isolation idle_timeout: 0, // Keep alive for test duration }); testDb = drizzle(testSql, { schema }); }); beforeEach(async () => { await testSql`BEGIN`; await testSql`SAVEPOINT test_savepoint`; }); afterEach(async () => { await testSql`ROLLBACK TO SAVEPOINT test_savepoint`; await testSql`ROLLBACK`; }); afterAll(async () => { await testSql.end(); }); } export function getTestDb() { return testDb; } ``` **Usage:** ```typescript describe("Subscription lifecycle", () => { withTestTransaction({ suiteName: "sub_lifecycle" }); it("creates active subscription", async () => { const db = getTestDb(); // Insert real data into real database const [user] = await db.insert(users).values({ email: "test@example.com", subscriptionStatus: "none", }).returning(); const [sub] = await db.insert(subscriptions).values({ userId: user.id, provider: "stripe", externalId: "sub_test_123", status: "active", currentPeriodStart: new Date(), currentPeriodEnd: new Date(Date.now() + 30 * 86400000), }).returning(); expect(sub.status).toBe("active"); expect(sub.userId).toBe(user.id); // Transaction rolls back automatically — no cleanup needed }); }); ``` --- ## Pattern 2: Test Data Factories Factories create REALISTIC test data, not minimal stubs. ```typescript // tests/utils/test-user-factory.ts import { randomUUID } from "node:crypto"; interface UserFactoryOptions { email?: string; subscriptionStatus?: SubscriptionStatus; isAdmin?: boolean; customerId?: string; } export async function createTestUser( db: PostgresJsDatabase, options: UserFactoryOptions = {} ): Promise<User> { const defaults = { id: randomUUID(), email: `test-${randomUUID().slice(0, 8)}@test.jeffreys-skills.md`, subscriptionStatus: "none" as const, isAdmin: false, customerId: `cus_test_${randomUUID().slice(0, 8)}`, displayName: "Test User", createdAt: new Date(), updatedAt: new Date(), }; const [user] = await db.insert(users) .values({ ...defaults, ...options }) .returning(); return user; } // Subscription factory — creates full subscription chain export async function createTestSubscription( db: PostgresJsDatabase, userId: string, options: Partial<typeof subscriptions.$inferInsert> = {} ): Promise<Subscription> { const defaults = { id: randomUUID(), userId, provider: "stripe" as const, externalId: `sub_test_${randomUUID().slice(0, 8)}`, status: "active" as const, currentPeriodStart: new Date(), currentPeriodEnd: new Date(Date.now() + 30 * 86400000), createdAt: new Date(), updatedAt: new Date(), }; const [sub] = await db.insert(subscriptions) .values({ ...defaults, ...options }) .returning(); // Also update the denormalized users table await db.update(users) .set({ subscriptionStatus: sub.status, subscriptionProvider: sub.provider, customerId: options.customerId ?? defaults.externalId, }) .where(eq(users.id, userId)); return sub; } ``` --- ## Pattern 3: Structured Test Logging Every mock-free test MUST produce structured logs. When a test fails in CI, you need to know the DB state, the timing, and the exact assertion failure. ```typescript // tests/utils/test-logger.ts export class TestLogger { testStart(name: string): void; phase(p: "setup" | "act" | "assert" | "teardown"): void; dbSnapshot(table: string, rows: unknown[], label?: string): void; assertMatch(field: string, expected: unknown, actual: unknown): boolean; testEnd(result: "pass" | "fail"): void; summary(): SuiteSummary; } // Vitest integration: auto-tracks test lifecycle export function withTestLogging(suite: string): TestLogger; ``` **Usage in tests:** ```typescript describe("Webhook idempotency", () => { const log = withTestLogging("webhook-idempotency"); withTestTransaction(); it("rejects duplicate webhook events", async () => { const db = getTestDb(); log.phase("setup"); const user = await createTestUser(db); const sub = await createTestSubscription(db, user.id); log.dbSnapshot("subscriptions", [sub], "before_webhook"); log.phase("act"); const event1 = await recordWebhookEvent({ provider: "stripe", eventId: "evt_123", eventType: "customer.subscription.updated", payload: {}, }); const event2 = await recordWebhookEvent({ provider: "stripe", eventId: "evt_123", // Same event ID — duplicate eventType: "customer.subscription.updated", payload: {}, }); log.phase("assert"); log.assertMatch("first event recorded", true, event1); log.assertMatch("duplicate rejected", false, event2); expect(event1).toBe(true); expect(event2).toBe(false); // Idempotency check }); }); ``` **JSON-line output (to stderr, machine-parseable):** ```json {"ts":"2026-03-23T07:10:11Z","suite":"webhook-idempotency","test":"rejects duplicate","phase":"setup","event":"phase_start"} {"ts":"2026-03-23T07:10:11Z","suite":"webhook-idempotency","test":"rejects duplicate","phase":"setup","event":"db_snapshot","data":{"table":"subscriptions","row_count":1}} {"ts":"2026-03-23T07:10:11Z","suite":"webhook-idempotency","test":"rejects duplicate","phase":"assert","event":"assertion","data":{"field":"first event recorded","expected":true,"actual":true,"match":true}} {"ts":"2026-03-23T07:10:11Z","suite":"webhook-idempotency","test":"rejects duplicate","event":"test_end","data":{"result":"pass","duration_ms":45}} ``` --- ## Pattern 4: Production Safety Guards **Non-negotiable.** Test infrastructure MUST block production URLs. ```typescript // tests/utils/real-db-harness.ts const PROD_URLS = [ "https://ircpwyadlwkkqivhegsy.supabase.co", // Production Supabase "https://auth.jeffreys-skills.md", // Production auth ]; export function getRealDbConfig(): { enabled: boolean; reason?: string } { if (env.REAL_API_TESTS !== "true") { return { enabled: false, reason: "REAL_API_TESTS not set" }; } if (env.NODE_ENV === "production") { return { enabled: false, reason: "NODE_ENV=production" }; } if (PROD_URLS.includes(env.NEXT_PUBLIC_SUPABASE_URL)) { return { enabled: false, reason: "Supabase URL is PRODUCTION" }; } return { enabled: true }; } ``` ```bash # Environment validation before running tests bun scripts/provision-test-env.ts --check # Validates: # - Supabase URL is NOT production # - Stripe key is sk_test_* (NOT sk_live_*) # - PayPal is sandbox mode # - NODE_ENV is NOT production ``` --- ## Pattern 5: Real Payment Provider Testing ```typescript // Test with Stripe test mode — real API, fake money describe("Stripe checkout flow", () => { withTestTransaction(); it("creates checkout session with test mode keys", async () => { // Uses sk_test_* key — no real charges const session = await stripe.checkout.sessions.create({ mode: "subscription", line_items: [{ price: env.STRIPE_PRICE_ID, quantity: 1 }], success_url: "http://localhost:3000/success", cancel_url: "http://localhost:3000/cancel", customer_email: "test@example.com", });
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub