提示:本网站现在为维护调试状态

Test Pyramid: Unit/Integration/E2E Ratios & Implementation

Choyeon
Choyeon
Sep 24, 2026 1 min read 574 Views
739 words 4,578 chars Updated Nov 8, 2026
Test Pyramid: Unit/Integration/E2E Ratios & Implementation
Table of contents

Anti-patterns (all-E2E or all-Mock) produce slow, flaky test suites. The classic pyramid solves this with layered structure: exponentially more tests as you go down, faster execution, tighter feedback loops - battle-proven methodology for large teams over a decade.

Unit tests at the bottom 70%: cover pure logic, edge cases, error branches. Mock all external deps; runs in milliseconds. Middle integration tests 20%: spin up real DB/Redis/message brokers via TestContainers. Verifies real service collaboration, no fakes. Top E2E 10%: Playwright/Cypress simulate real users on only critical business paths (login→order→pay), skip edge branches entirely.

// ====== 单元测试 (pactum + vitest + mocking) ======
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { OrderService } from './order.service';
import type { PaymentGateway, InventoryRepo, OrderRepo } from './ports';

const makeSUT = () => {
  const payment = { charge: vi.fn().mockResolvedValue({ success: true, txId: 'tx_001' }) } as unknown as PaymentGateway;
  const inventory = { lockStock: vi.fn().mockResolvedValue(true), releaseStock: vi.fn() } as unknown as InventoryRepo;
  const orders = { save: vi.fn().mockImplementation(o => Promise.resolve({ ...o, id: 'ord_42' })) } as unknown as OrderRepo;
  return { sut: new OrderService(orders, inventory, payment), payment, inventory, orders };
};

describe('OrderService.placeOrder 单元测试', () => {
  it('正常下单: 扣库存-扣款-落库三阶段成功', async () => {
    const { sut, orders, inventory, payment } = makeSUT();
    const order = await sut.placeOrder({ userId: 'u1', items: [{ sku: 'SK1', qty: 2, unitPrice: 99 }] });
    expect(order.id).toBe('ord_42');
    expect(inventory.lockStock).toHaveBeenCalledWith('SK1', 2);
    expect(payment.charge).toHaveBeenCalledWith(198, expect.any(String));
    expect(orders.save).toHaveBeenCalledTimes(1);
  });
  it('扣款失败时自动回滚库存释放', async () => {
    const { sut, payment, inventory } = makeSUT();
    payment.charge.mockResolvedValueOnce({ success: false, txId: 'tx_002', reason: 'INSUFFICIENT_FUNDS' });
    await expect(sut.placeOrder({ userId: 'u1', items: [{ sku: 'SK1', qty: 1, unitPrice: 99 }] }))
      .rejects.toThrow(/PAYMENT_FAILED/);
    expect(inventory.releaseStock).toHaveBeenCalledWith('SK1', 1);
  });
});

// ====== 集成测试 (TestContainer 启动真实 Postgres + Redis) ======
import { PostgreSqlContainer, RedisContainer } from '@testcontainers/postgresql';
import { createClient } from 'redis';

describe('CartRepository 集成测试', () => {
  let pgContainer: PostgreSqlContainer, redisContainer: RedisContainer;
  let pool: any, redis: ReturnType<typeof createClient>;

  beforeAll(async () => {
    pgContainer = await new PostgreSqlContainer('postgres:16-alpine').withDatabase('testdb').start();
    redisContainer = await new RedisContainer('redis:7-alpine').start();
    pool = require('pg').Pool({ connectionString: pgContainer.getConnectionUri() });
    await pool.query('CREATE TABLE carts (user_id TEXT PRIMARY KEY, items JSONB NOT NULL, updated_at TIMESTAMPTZ DEFAULT NOW())');
    redis = createClient({ url: redisContainer.getConnectionUrl() });
    await redis.connect();
  }, 60000);

  it('写 PG 后读 Redis 双写一致 (100 次并发下)', async () => {
    const repo = new (require('./cart.repo').CartRepository)(pool, redis);
    const N = 100;
    await Promise.all(Array.from({ length: N }, (_, i) => repo.upsert(`user_${i}`, [{ sku: `S${i}`, qty: i % 5 + 1 }])));
    for (let i = 0; i < N; i++) {
      const cached = JSON.parse(await redis.get(`cart:user_${i}`) || 'null');
      const row = (await pool.query('SELECT items FROM carts WHERE user_id=$1', [`user_${i}`])).rows[0];
      expect(cached).toMatchObject(row.items);
    }
  });

  afterAll(async () => {
    await pool?.end(); await redis?.quit();
    await pgContainer?.stop(); await redisContainer?.stop();
  });
});

// ====== E2E 测试 (Playwright - 仅核心 happy path) ======
import { test, expect } from '@playwright/test';

test.describe('结账核心链路 E2E', () => {
  test('登录→加购→结账→订单成功页完整路径', async ({ page }) => {
    await page.goto('/login');
    await page.fill('input[name="email"]', 'e2e-user@example.com');
    await page.fill('input[name="password"]', 'ValidPass123!');
    await page.click('button[type="submit"]');
    await expect(page).toHaveURL('/products');
    await page.click('[data-testid="product-42-add"]');
    await expect(page.locator('[data-testid="cart-count"]')).toHaveText('1');
    await page.goto('/checkout');
    await page.fill('[data-testid="card-number"]', '4242424242424242');
    await page.fill('[data-testid="card-cvc"]', '123');
    await page.click('[data-testid="submit-order"]');
    await expect(page.locator('[data-testid="order-success"]')).toBeVisible({ timeout: 15000 });
  });
});

Coverage Metrics and Adoption Cadence

Don't worship 100% line coverage. Practical targets: unit line coverage 70-85%, branch coverage 90%+ on critical modules; integration tests stress writes (order submission, payment callbacks) and consistency; E2E is driven by business P0 checklist. Follow the 2/3/5 rule: all unit tests under 2s, integration done under 3 min, core E2E within 5 min.

Layer Volume Ratio Per-Run Speed Failure Indicates Owner
Unit 70% <50ms Function-level logic bug Dev self-test, CI gate
Integration 20% <3s Service collaboration / DB schema drift Dev + QA
E2E 10% <1min End-to-end regression / build broken QA + release gate

Best Practices

Define contracts before implementation (TDD). New features start with failing E2E spec, drill down layer by layer. CI stages: PR runs unit+integration immediately, release branch runs E2E only so daily iteration stays unblocked.

X微博Telegram

About the author

Poll

你最希望 Rosetta 优先加入哪项功能?

社区投票(演示数据,可后台删除)

Comments (3)

0 / 3000
考研加油鸭
考研加油鸭kaoyan-journey.xyzSep 22, 2026

哈哈,楼主的踩坑经历和我一模一样!我当时就是没注意到这个配置,排查了整整两天,看到这篇文章泪目了。

自由撰稿人阿秋
自由撰稿人阿秋qiuwrites.blogSep 21, 2026

写得太好了,终于搞懂了分布式事务的几种方案区别,收藏了!

独立开发者阿明
独立开发者阿明aming.devSep 21, 2026

请问一下,文中提到的连接池配置,如果是在高并发场景下,一般设置多少比较合适?

Related posts

PostgreSQL Query Optimization Techniques

PostgreSQL Query Optimization Techniques

Master PostgreSQL optimization: EXPLAIN ANALYZE plans, choosing index types (B-tree/GIN/BRIN), updating statistics, tuning joins, and partitioned table design for maximum performance.

1 min read
Advanced TypeScript Type Gymnastics

Advanced TypeScript Type Gymnastics

TypeScript's type system rivals a full programming language. This article systematically covers conditional types, mapped types, template literal types, infer inference, recursive types, distributive conditional types, and more. With 15+ real utility type implementations (DeepPartial, PathOf, PickByType, CamelCase, etc.), master solution approaches for type-challenges at intermediate difficulty and above.

1 min read
Rust Ownership & Borrowing: Lifetimes Explained

Rust Ownership & Borrowing: Lifetimes Explained

Master Rust's most unique concepts: ownership rules, mutable/immutable borrowing constraints, lifetime annotations. Compile-error examples + fixes to think like the Borrow Checker for safe zero-cost code.

1 min read
Zero Trust Security Architecture: Never Trust, Always Verify

Zero Trust Security Architecture: Never Trust, Always Verify

Zero Trust security model primer: built on "never trust, always verify" and dismantles traditional VPN-equals-trusted perimeter thinking. Six pillars explained — identity (MFA/SAML), device health checks, least-privilege RBAC, microsegmentation mTLS, continuous trust scoring, observable audit trail — with implementation roadmap.

1 min read
Getting Started with WebAssembly: Rust to WASM + JS

Getting Started with WebAssembly: Rust to WASM + JS

End-to-end Rust to WASM setup: wasm-pack toolchain, wasm-bindgen type bindings, JS sync/async invocation, TypedArray memory sharing for compute-heavy browser workloads.

1 min read
K8s Pod Lifecycle, Init Containers & Sidecars

K8s Pod Lifecycle, Init Containers & Sidecars

Full K8s Pod state machine Pending→Terminated, PostStart/PreStop hooks, Init Container dependency guarantees, Sidecar patterns (istio-proxy, log collectors), and zero-downtime graceful termination configs.

1 min read