[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"public:announcements":3,"public:navigations:zh":4,"post:detail:post-27-测试金字塔单元集:zh":56,"footer:categories:zh":103},[],[5,18,25,33,41,49],{"id":6,"title":7,"url":12,"icon":13,"parent_id":13,"location":14,"order":6,"is_active":15,"target_blank":16,"created_at":17,"updated_at":13},1,{"zh":8,"en":9,"ja":10,"zh_TW":11},"首页","Home","ホーム","首頁","\u002F",null,"header",true,false,"2026-09-30T18:09:54.251095Z",{"id":19,"title":20,"url":24,"icon":13,"parent_id":13,"location":14,"order":19,"is_active":15,"target_blank":16,"created_at":17,"updated_at":13},2,{"zh":21,"en":22,"ja":23,"zh_TW":21},"文章","Posts","記事","\u002Fposts",{"id":26,"title":27,"url":32,"icon":13,"parent_id":13,"location":14,"order":26,"is_active":15,"target_blank":16,"created_at":17,"updated_at":13},3,{"zh":28,"en":29,"ja":30,"zh_TW":31},"分类","Categories","カテゴリー","分類","\u002Fcategories",{"id":34,"title":35,"url":40,"icon":13,"parent_id":13,"location":14,"order":34,"is_active":15,"target_blank":16,"created_at":17,"updated_at":13},4,{"zh":36,"en":37,"ja":38,"zh_TW":39},"标签","Tags","タグ","標籤","\u002Ftags",{"id":42,"title":43,"url":48,"icon":13,"parent_id":13,"location":14,"order":42,"is_active":15,"target_blank":16,"created_at":17,"updated_at":13},5,{"zh":44,"en":45,"ja":46,"zh_TW":47},"关于","About","概要","關於","\u002Fpage\u002Fabout",{"id":50,"title":51,"url":55,"icon":13,"parent_id":13,"location":14,"order":50,"is_active":15,"target_blank":16,"created_at":17,"updated_at":13},6,{"zh":52,"en":53,"ja":54,"zh_TW":52},"留言板","Guestbook","ゲストブック","\u002Fpage\u002Fguestbook",{"id":57,"title":58,"subtitle":13,"slug":59,"source":60,"source_url":13,"audio":13,"video":13,"video_url":13,"content":61,"excerpt":62,"cover_image":13,"author":63,"category":66,"tags":74,"status":97,"visibility":98,"password":13,"views":99,"likes_count":73,"is_pinned":16,"allow_comments":15,"comments_count":26,"is_password_protected":16,"meta_title":58,"meta_description":62,"meta_keywords":100,"created_at":101,"published_at":101,"updated_at":102,"reading_time":6},27,"测试金字塔：单元\u002F集成\u002FE2E 测试比例与落地","post-27-测试金字塔单元集","原创","# 测试金字塔：单元\u002F集成\u002FE2E 测试比例与落地\n\n测试反模式（全是 E2E 全是 Mock）都会导致测试又慢又不稳。经典测试金字塔用分层结构解决：数量从上到下指数增加，速度越来越快，反馈周期越来越短，是大型团队十年验证过的稳定方法论。\n\n## 三层分工与推荐比例\n\n底层单元测试占 70%：覆盖纯逻辑、边界条件、错误分支，Mock 所有外部依赖，毫秒级跑完。中层集成测试 20%：用 TestContainer 启动真实数据库\u002FRedis\u002F消息队列，测服务之间真实协作不造假。顶层 E2E 10%：Playwright\u002FCypress 模拟真实用户，只覆盖核心业务路径（登录-下单-支付），不跑边角分支。\n\n```typescript\n```typescript\n\u002F\u002F ====== 单元测试 (pactum + vitest + mocking) ======\nimport { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { OrderService } from '.\u002Forder.service';\nimport type { PaymentGateway, InventoryRepo, OrderRepo } from '.\u002Fports';\n\nconst makeSUT = () => {\n  const payment = { charge: vi.fn().mockResolvedValue({ success: true, txId: 'tx_001' }) } as unknown as PaymentGateway;\n  const inventory = { lockStock: vi.fn().mockResolvedValue(true), releaseStock: vi.fn() } as unknown as InventoryRepo;\n  const orders = { save: vi.fn().mockImplementation(o => Promise.resolve({ ...o, id: 'ord_42' })) } as unknown as OrderRepo;\n  return { sut: new OrderService(orders, inventory, payment), payment, inventory, orders };\n};\n\ndescribe('OrderService.placeOrder 单元测试', () => {\n  it('正常下单: 扣库存-扣款-落库三阶段成功', async () => {\n    const { sut, orders, inventory, payment } = makeSUT();\n    const order = await sut.placeOrder({ userId: 'u1', items: [{ sku: 'SK1', qty: 2, unitPrice: 99 }] });\n    expect(order.id).toBe('ord_42');\n    expect(inventory.lockStock).toHaveBeenCalledWith('SK1', 2);\n    expect(payment.charge).toHaveBeenCalledWith(198, expect.any(String));\n    expect(orders.save).toHaveBeenCalledTimes(1);\n  });\n  it('扣款失败时自动回滚库存释放', async () => {\n    const { sut, payment, inventory } = makeSUT();\n    payment.charge.mockResolvedValueOnce({ success: false, txId: 'tx_002', reason: 'INSUFFICIENT_FUNDS' });\n    await expect(sut.placeOrder({ userId: 'u1', items: [{ sku: 'SK1', qty: 1, unitPrice: 99 }] }))\n      .rejects.toThrow(\u002FPAYMENT_FAILED\u002F);\n    expect(inventory.releaseStock).toHaveBeenCalledWith('SK1', 1);\n  });\n});\n\n\u002F\u002F ====== 集成测试 (TestContainer 启动真实 Postgres + Redis) ======\nimport { PostgreSqlContainer, RedisContainer } from '@testcontainers\u002Fpostgresql';\nimport { createClient } from 'redis';\n\ndescribe('CartRepository 集成测试', () => {\n  let pgContainer: PostgreSqlContainer, redisContainer: RedisContainer;\n  let pool: any, redis: ReturnType\u003Ctypeof createClient>;\n\n  beforeAll(async () => {\n    pgContainer = await new PostgreSqlContainer('postgres:16-alpine').withDatabase('testdb').start();\n    redisContainer = await new RedisContainer('redis:7-alpine').start();\n    pool = require('pg').Pool({ connectionString: pgContainer.getConnectionUri() });\n    await pool.query('CREATE TABLE carts (user_id TEXT PRIMARY KEY, items JSONB NOT NULL, updated_at TIMESTAMPTZ DEFAULT NOW())');\n    redis = createClient({ url: redisContainer.getConnectionUrl() });\n    await redis.connect();\n  }, 60000);\n\n  it('写 PG 后读 Redis 双写一致 (100 次并发下)', async () => {\n    const repo = new (require('.\u002Fcart.repo').CartRepository)(pool, redis);\n    const N = 100;\n    await Promise.all(Array.from({ length: N }, (_, i) => repo.upsert(`user_${i}`, [{ sku: `S${i}`, qty: i % 5 + 1 }])));\n    for (let i = 0; i \u003C N; i++) {\n      const cached = JSON.parse(await redis.get(`cart:user_${i}`) || 'null');\n      const row = (await pool.query('SELECT items FROM carts WHERE user_id=$1', [`user_${i}`])).rows[0];\n      expect(cached).toMatchObject(row.items);\n    }\n  });\n\n  afterAll(async () => {\n    await pool?.end(); await redis?.quit();\n    await pgContainer?.stop(); await redisContainer?.stop();\n  });\n});\n\n\u002F\u002F ====== E2E 测试 (Playwright - 仅核心 happy path) ======\nimport { test, expect } from '@playwright\u002Ftest';\n\ntest.describe('结账核心链路 E2E', () => {\n  test('登录→加购→结账→订单成功页完整路径', async ({ page }) => {\n    await page.goto('\u002Flogin');\n    await page.fill('input[name=\"email\"]', 'e2e-user@example.com');\n    await page.fill('input[name=\"password\"]', 'ValidPass123!');\n    await page.click('button[type=\"submit\"]');\n    await expect(page).toHaveURL('\u002Fproducts');\n    await page.click('[data-testid=\"product-42-add\"]');\n    await expect(page.locator('[data-testid=\"cart-count\"]')).toHaveText('1');\n    await page.goto('\u002Fcheckout');\n    await page.fill('[data-testid=\"card-number\"]', '4242424242424242');\n    await page.fill('[data-testid=\"card-cvc\"]', '123');\n    await page.click('[data-testid=\"submit-order\"]');\n    await expect(page.locator('[data-testid=\"order-success\"]')).toBeVisible({ timeout: 15000 });\n  });\n});\n```\n```\n\n## 覆盖率指标与落地节奏\n\n不要迷信 100% 行覆盖率。实际推荐：单元测试行覆盖 70-85%，重点模块分支覆盖 90%+；集成测试重点测写操作（提交订单\u002F支付回调）和一致性；E2E 按业务 P0 清单来。按 2\u002F3\u002F5 法则：2 秒内跑完全部单测，3 分钟内跑完集成，5 分钟内跑完核心 E2E。\n\n| 层级 | 数量比例 | 单次速度 | 失败含义 | 归属负责人 |\n|------|---------|---------|---------|-----------|\n| 单元测试 | 70% | \u003C50ms | 函数级逻辑bug | 开发自测CI必过 |\n| 集成测试 | 20% | \u003C3s | 服务协作bug\u002FDBSchema不一致 | 开发 + QA |\n| E2E 测试 | 10% | \u003C1min | 链路级回归\u002F构建产物损坏 | QA + 发布门禁 |\n\n## 最佳实践\n\n先写契约再写实现（TDD），新需求从 E2E 失败用例开始驱动，逐层下沉细化。CI 分层执行：PR 提交先跑单测+集成，发布分支再跑 E2E，避免阻塞日常迭代。","系统解析测试金字塔架构：从底层单元测试（大量快速、Mock 隔离依赖）、中层集成测试（服务\u002F数据库\u002F消息协作真实交互）、顶层 E2E 测试（真实用户路径少量精测）的推荐比例 70% \u002F 20% \u002F 10%，附项目覆盖率指标与落地实践。",{"id":6,"username":64,"nickname":64,"avatar":13,"cover_image":13,"bio":13,"website":13,"github":13,"avatar_source":13,"resolved_avatar_url":13,"title":13,"created_at":65},"Choyeon","2026-09-30T18:09:53.330367Z",{"id":6,"name":67,"slug":68,"description":69,"icon":70,"color":71,"cover_image":13,"created_at":72,"post_count":73},"技术","technology","前沿技术探索与深度分析，涵盖编程语言、框架、架构设计等核心领域","heroicons:cog-6-tooth","#6366f1","2026-09-30T18:09:53.633610Z",0,[75,80,85,91],{"id":6,"name":76,"slug":77,"color":78,"icon":13,"is_active":15,"created_at":79,"post_count":73},"Python","python","#3776ab","2026-09-30T18:09:53.663835Z",{"id":50,"name":81,"slug":82,"color":83,"icon":13,"is_active":15,"created_at":84,"post_count":73},"TypeScript","typescript","#3178c6","2026-09-30T18:09:53.680697Z",{"id":86,"name":87,"slug":88,"color":89,"icon":13,"is_active":15,"created_at":90,"post_count":73},8,"Node.js","nodejs","#339933","2026-09-30T18:09:53.685769Z",{"id":92,"name":93,"slug":94,"color":95,"icon":13,"is_active":15,"created_at":96,"post_count":73},13,"算法","algorithms","#ef4444","2026-09-30T18:09:53.700921Z","published","public",418,"Node.js,Python,TypeScript,算法","2026-09-23T16:09:53.733462Z","2026-10-01T10:32:41.719776Z",[104,113,127,142,157,172,187,201],{"id":6,"name":105,"slug":68,"description":109,"icon":70,"color":71,"cover_image":13,"created_at":72,"post_count":86},{"en":106,"ja":107,"zh":67,"zh_Hant":108},"Technology","テクノロジー","技術",{"en":110,"ja":111,"zh":69,"zh_Hant":112},"Cutting-edge technology exploration and in-depth analysis covering programming languages, frameworks, and architecture design","プログラミング言語、フレームワーク、アーキテクチャ設計を含む先端技術の探究","前沿技術探索與深度分析，涵蓋編程語言、框架、架構設計等核心領域",{"id":19,"name":114,"slug":118,"description":119,"icon":124,"color":125,"cover_image":13,"created_at":126,"post_count":50},{"en":115,"ja":116,"zh":117,"zh_Hant":117},"Tutorial","チュートリアル","教程","tutorial",{"en":120,"ja":121,"zh":122,"zh_Hant":123},"Hands-on tutorials from scratch with clear steps and complete code for developers at all levels","ゼロから始める実践的なチュートリアル。明確な手順と完全なコードで、全てのレベルの開発者に最適","从零开始的实战教程，步骤清晰，代码完整，适合各阶段开发者学习","從零開始的實戰教程，步驟清晰，代碼完整，適合各階段開發者學習","heroicons:academic-cap","#10b981","2026-09-30T18:09:53.641581Z",{"id":26,"name":128,"slug":132,"description":133,"icon":138,"color":139,"cover_image":13,"created_at":140,"post_count":141},{"en":129,"ja":130,"zh":131,"zh_Hant":131},"Frontend","フロントエンド","前端","frontend",{"en":134,"ja":135,"zh":136,"zh_Hant":137},"Full coverage of web frontend development including Vue, React, new CSS features, and performance optimization","Vue、React、CSSの新機能やパフォーマンス最適化を含むWebフロントエンド開発の全領域","Web前端开发全栈覆盖，包括Vue、React、CSS新特性及性能优化等话题","Web前端開發全棧覆蓋，包括Vue、React、CSS新特性及性能優化等話題","heroicons:computer-desktop","#ec4899","2026-09-30T18:09:53.644838Z",7,{"id":34,"name":143,"slug":148,"description":149,"icon":154,"color":155,"cover_image":13,"created_at":156,"post_count":26},{"en":144,"ja":145,"zh":146,"zh_Hant":147},"Backend","バックエンド","后端","後端","backend",{"en":150,"ja":151,"zh":152,"zh_Hant":153},"Server-side development best practices including API design, database optimization, and microservices architecture","API設計、データベース最適化、マイクロサービスアーキテクチャを含むサーバーサイド開発の実践","服务端开发实战经验，API设计、数据库优化、微服务架构等深度内容","服務端開發實戰經驗，API設計、數據庫優化、微服務架構等深度內容","heroicons:server-stack","#f59e0b","2026-09-30T18:09:53.647836Z",{"id":42,"name":158,"slug":163,"description":164,"icon":169,"color":170,"cover_image":13,"created_at":171,"post_count":73},{"en":159,"ja":160,"zh":161,"zh_Hant":162},"Fullstack","フルスタック","全栈","全棧","fullstack",{"en":165,"ja":166,"zh":167,"zh_Hant":168},"End-to-end development guides covering the full project lifecycle and technology stack selection","フロントエンドからバックエンドまでの開発プロセス全体をカバーするガイド","从前端到后端的全流程开发指南，覆盖完整项目生命周期与技术选型","從前端到後端的全流程開發指南，覆蓋完整項目生命週期與技術選型","heroicons:swatch","#8b5cf6","2026-09-30T18:09:53.650513Z",{"id":50,"name":173,"slug":178,"description":179,"icon":184,"color":185,"cover_image":13,"created_at":186,"post_count":34},{"en":174,"ja":175,"zh":176,"zh_Hant":177},"Essays","エッセイ","随笔","隨筆","essays",{"en":180,"ja":181,"zh":182,"zh_Hant":183},"Technical reflections and development insights recording the programmer's growth journey","プログラマーの成長の軌跡を記録する技術的考察と開発の洞察","技术思考与开发心得分享，记录程序员成长路上的点滴感悟","技術思考與開發心得分享，記錄程序員成長路上的點滴感悟","heroicons:pencil-square","#f43f5e","2026-09-30T18:09:53.653105Z",{"id":141,"name":188,"slug":192,"description":193,"icon":198,"color":199,"cover_image":13,"created_at":200,"post_count":34},{"en":189,"ja":190,"zh":191,"zh_Hant":191},"Tools","ツール","工具","tools",{"en":194,"ja":195,"zh":196,"zh_Hant":197},"Development toolchain and productivity tool recommendations to supercharge your development experience","開発効率を飛躍的に向上させるツールチェーンと生産性ツールの推奨","开发工具链与效率提升利器推荐，让你的开发体验如虎添翼","開發工具鏈與效率提升利器推薦，讓你的開發體驗如虎添翼","heroicons:wrench-screwdriver","#06b6d4","2026-09-30T18:09:53.655605Z",{"id":86,"name":202,"slug":207,"description":208,"icon":213,"color":214,"cover_image":13,"created_at":215,"post_count":73},{"en":203,"ja":204,"zh":205,"zh_Hant":206},"Translation","翻訳","翻译","翻譯","translation",{"en":209,"ja":210,"zh":211,"zh_Hant":212},"Curated translations of high-quality foreign technical articles to keep up with international trends","国際的な技術トレンドを追うための厳選された海外技術記事の翻訳","优质外文技术文章精选翻译，紧跟国际技术前沿动态","優質外文技術文章精選翻譯，緊跟國際技術前沿動態","heroicons:language","#84cc16","2026-09-30T18:09:53.658041Z"]