Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

Back to skills

Testing Real Service E2e No Mocks

ASecurity

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.

22 stars
0 votes
0 copies
0 views
Added 9/20/2026
testingtypescriptgobashsqlnodetestingapidatabase

Works with

cliapi

Security Analysis

A100/100

Scanned 9/20/2026

Install to Claude Code

$npx -y skills add lev-os/agents --skill testing-real-service-e2e-no-mocks --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Testing Real Service E2e No Mocks?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Testing Real Service E2e No Mocks
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/lev-os-testing-real-service-e2e-no-mocks/badge)](https://www.skillsdirectory.com/skills/lev-os-testing-real-service-e2e-no-mocks)

More formats (shields.io, HTML) on the badges page.

Download Zip
Files
SKILL.md
---
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",
    });

    expect(session.id).toMatch(/^cs_test_/);
    expect(session.url).toBeTruthy();
  });
});
```

---

## Pattern 6: Cleanup Registry (Safety Net)

For tests that can't use transaction rollback (e.g., cross-service tests):

```typescript
class CleanupRegistry {
  private entries: Array<{ type: string; id: string }> = [];

  track(type: string, id: string): void {
    this.entries.push({ type, id });
  }

  async cleanup(): Promise<{ cleaned: number; failed: number }> {
    let cleaned = 0, failed = 0;
    // Cleanup in reverse order (LIFO — respects FK constraints)
    for (const entry of this.entries.reverse()) {
      try {
        await this.deleteByType(entry.type, entry.id);
        cleaned++;
      } catch (e) {
        failed++;
      }
    }
    this.entries = [];
    return { cleaned, failed };
  }
}
```

---

## The Migration Path: Mocked → Mock-Free

When replacing an existing mocked test with a real-DB test:

```
1. READ the mocked test — understand WHAT it tests (not HOW)
2. IDENTIFY the assertions — what behavior is being verified?
3. CHECK if mock constants match reality:
   - Are mocked response shapes current? (schema drift?)
   - Are mocked status values current? (enum changes?)
   - Are mocked business rules current? (policy changes?)
4. WRITE the mock-free version using real DB + factories
5. RUN BOTH side by side — same assertions, different infrastructure
6. VERIFY the mock-free version catches bugs the mock didn't
7. DELETE the mocked version (don't keep both!)
```

**Common mock-reality divergences that cause production bugs:**
- Mocked `onDelete: "cascade"` — mock doesn't cascade, real DB does
- Mocked subscription status — mock allows transitions that business logic prevents
- Mocked webhook payloads — real webhook has fields the mock doesn't include
- Mocked email templates — mock doesn't render, so template errors go undetected

---

## Anti-Patterns (Hard Constraints)

| ✗ Never | Why | Fix |
|---------|-----|-----|
| Mock the database for billing tests | Hides cascade, FK, and transaction bugs | Transaction rollback isolation |
| Share state between tests | Order-dependent failures, flaky tests | Each test gets its own transaction |
| Skip structured logging | CI failures are undebuggable | JSON-line output with phase/timing/snapshots |
| Use production URLs in test env | One bad test can charge real money or delete real data | Production URL blocklist |
| Keep both mocked and mock-free versions | Maintenance burden, false confidence | Delete the mock when mock-free is proven |
| Hard-code test data | Fragile, doesn't catch edge cases | Use factories with randomized defaults |
| Forget cleanup for cross-service tests | Test data accumulates, contaminates MRR | Cleanup registry with LIFO ordering |
| Test against `localhost:3000` without running server | Silent pass with no actual verification | CI must start dev server or use direct imports |

---

## Checklist (Before Shipping Mock-Free Test Suite)

- [ ] Transaction rollback isolation for every test
- [ ] Test data factories (not hard-coded data)
- [ ] Structured JSON-line logging (phase, timing, DB snapshots)
- [ ] Production URL blocklist in test harness
- [ ] Stripe/PayPal use test mode keys (sk_test_*, sandbox)
- [ ] `provision-test-env.ts --check` validates all required env vars
- [ ] Mock Risk Matrix scored: all Score ≥ 8 paths are mock-free
- [ ] Cleanup registry for tests that can't use transactions
- [ ] Every production bug has a mock-free regression test
- [ ] Mocked versions deleted after mock-free is proven working
- [ ] CI runs mock-free tests on every PR

---

## References

| Need | Reference |
|------|-----------|
| Transaction rollback deep-dive | [TRANSACTION-ISOLATION.md](references/TRANSACTION-ISOLATION.md) |
| Factory patterns catalog | [FACTORIES.md](references/FACTORIES.md) |
| Structured logging formats | [LOGGING-FORMATS.md](references/LOGGING-FORMATS.md) |
| Payment provider test modes | [PAYMENT-TESTING.md](references/PAYMENT-TESTING.md) |
| Migration playbook: mock → real | [MIGRATION-PLAYBOOK.md](references/MIGRATION-PLAYBOOK.md) |

## Relationship to Other Testing Skills

| Technique | Use INSTEAD when | Use TOGETHER when |
|-----------|-----------------|-------------------|
| /testing-golden-artifacts | Output is complex, needs frozen reference | Goldens verify output of mock-free tests |
| /testing-conformance-harnesses | Testing against a spec, not just correctness | Conformance suite uses real DB harness |
| /testing-fuzzing | Finding crashes in parsers | Fuzz-found crashes become mock-free regression tests |
| /testing-metamorphic | Can't compute expected output | MRs validate relations in mock-free integration tests |
| /e2e-testing-for-webapps | Browser-level testing | E2E flows use same real-DB infrastructure |

Attribution

lev-oslev-os
View sourceMore from lev-os →
SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

Related Skills

Screen Reader Testing

Practical guide to testing web applications with screen readers for comprehensive accessibility validation.

393431 votes

Python Testing

使用pytest、TDD方法、夹具、模拟、参数化和覆盖率要求的Python测试策略。

2456590 votes

Tdd Workflow

在编写新功能、修复错误或重构代码时使用此技能。强制执行测试驱动开发,包含单元测试、集成测试和端到端测试,覆盖率超过80%。

2456590 votes

Springboot Tdd

使用JUnit 5、Mockito、MockMvc、Testcontainers和JaCoCo进行Spring Boot的测试驱动开发。适用于添加功能、修复错误或重构时。

2456590 votes

Eval Harness

克劳德代码会话的正式评估框架,实施评估驱动开发(EDD)原则

2456590 votes
View all in testing →