Skip to content

游戏测试专题

预计阅读 25 分钟
版本信息
最后更新:2026-07-09 · 适用基础库:≥ 3.9.0(最低兼容 ≥ 2.30.0)

游戏测试往往被独立开发者忽视,但它是保证产品质量和用户体验的关键环节。本章讲解如何将前端测试经验迁移到游戏开发中。


前端测试 vs 游戏测试

前端对比:Web 测试 vs 游戏测试
维度Web 前端测试游戏测试
单元测试对象组件逻辑、工具函数游戏逻辑(碰撞检测、计分、状态转换)
组件/实体测试React Testing Library、Vue Test Utils引擎组件测试(Cocos/Unity)
E2E 测试Playwright、Cypressminium(微信官方)、AutoTest
视觉回归Percy、Chromatic 截图对比Canvas/WebGL 截图像素级对比
性能测试Lighthouse CI帧率稳定性、内存泄漏检测、首帧耗时
测试难点异步状态、DOM 渲染帧依赖逻辑、物理引擎随机性、真机兼容性
覆盖率目标80%+(行覆盖)核心玩法 90%+,UI/渲染层选择性覆盖

关键差异: Web 应用测试的核心是"给定 Props,验证 DOM 输出";游戏测试的核心是"给定输入序列和初始状态,验证多帧后的状态和渲染结果"。游戏的时序依赖和物理随机性使得传统的快照测试难以直接套用。


单元测试:核心游戏逻辑

测试环境搭建

typescript
// 使用 Jest + ts-jest 测试游戏核心逻辑
// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node', // 游戏逻辑不依赖 DOM
  roots: ['<rootDir>/tests'],
  testMatch: ['**/*.test.ts'],
  moduleNameMapper: {
    // 对引擎 API 做 mock
    '^cc$': '<rootDir>/tests/__mocks__/cc.ts',
  },
}

碰撞检测测试

typescript
// tests/collision.test.ts
import { rectCollision, circleCollision } from '../src/utils'

describe('碰撞检测', () => {
  describe('矩形碰撞 (AABB)', () => {
    test('两个矩形完全重叠时返回 true', () => {
      expect(rectCollision(0, 0, 10, 10, 5, 5, 10, 10)).toBe(true)
    })

    test('两个矩形相邻但不重叠时返回 false', () => {
      // rect1: (0,0,10,10), rect2: (10,0,10,10) — 边界相邻
      expect(rectCollision(0, 0, 10, 10, 10, 0, 10, 10)).toBe(false)
    })

    test('一个矩形完全在另一个内部时返回 true', () => {
      expect(rectCollision(0, 0, 20, 20, 5, 5, 5, 5)).toBe(true)
    })

    test('两个矩形距离很远时返回 false', () => {
      expect(rectCollision(0, 0, 10, 10, 100, 100, 10, 10)).toBe(false)
    })

    test('同一条边对齐且有部分重叠时返回 true', () => {
      expect(rectCollision(0, 0, 10, 10, 5, 0, 15, 10)).toBe(true)
    })
  })

  describe('圆形碰撞', () => {
    test('两个圆重叠时返回 true', () => {
      expect(circleCollision(0, 0, 10, 15, 0, 10)).toBe(true) // 圆心距 15 < 半径和 20
    })

    test('两个圆相切时返回 true', () => {
      expect(circleCollision(0, 0, 10, 20, 0, 10)).toBe(true) // 圆心距 20 == 半径和 20
    })

    test('两个圆不重叠时返回 false', () => {
      expect(circleCollision(0, 0, 10, 30, 0, 10)).toBe(false) // 圆心距 30 > 半径和 20
    })
  })
})

游戏状态机测试

typescript
// tests/game-state.test.ts
enum GameState {
  Ready,
  Playing,
  Paused,
  GameOver,
}

class GameStateMachine {
  private state = GameState.Ready

  // 允许的状态转换表
  private readonly transitions: Record<GameState, GameState[]> = {
    [GameState.Ready]: [GameState.Playing],
    [GameState.Playing]: [GameState.Paused, GameState.GameOver],
    [GameState.Paused]: [GameState.Playing, GameState.Ready],
    [GameState.GameOver]: [GameState.Ready],
  }

  get current() {
    return this.state
  }

  transition(to: GameState): boolean {
    if (this.transitions[this.state].includes(to)) {
      this.state = to
      return true
    }
    return false
  }
}

describe('游戏状态机', () => {
  let fsm: GameStateMachine

  beforeEach(() => {
    fsm = new GameStateMachine()
  })

  test('初始状态为 Ready', () => {
    expect(fsm.current).toBe(GameState.Ready)
  })

  test('Ready → Playing 是合法转换', () => {
    expect(fsm.transition(GameState.Playing)).toBe(true)
    expect(fsm.current).toBe(GameState.Playing)
  })

  test('Playing → GameOver 是合法转换', () => {
    fsm.transition(GameState.Playing)
    expect(fsm.transition(GameState.GameOver)).toBe(true)
    expect(fsm.current).toBe(GameState.GameOver)
  })

  test('GameOver → Ready 是合法转换(重新开始)', () => {
    fsm.transition(GameState.Playing)
    fsm.transition(GameState.GameOver)
    expect(fsm.transition(GameState.Ready)).toBe(true)
  })

  test('Ready → GameOver 是非法转换', () => {
    expect(fsm.transition(GameState.GameOver)).toBe(false)
    expect(fsm.current).toBe(GameState.Ready) // 状态未变
  })

  test('Playing → Ready 是非法转换(必须经过 GameOver)', () => {
    fsm.transition(GameState.Playing)
    expect(fsm.transition(GameState.Ready)).toBe(false)
    expect(fsm.current).toBe(GameState.Playing)
  })
})

计分逻辑测试

typescript
// tests/scoring.test.ts
describe('计分系统', () => {
  // 测试 Flappy Bird 的通过管道计分逻辑
  test('小鸟通过管道中心线时加 1 分', () => {
    const birdX = 120
    const pipes = [
      { x: 100, width: 60, passed: false },
      { x: 200, width: 60, passed: false },
    ]

    let score = 0
    for (const pipe of pipes) {
      if (!pipe.passed && birdX > pipe.x + pipe.width) {
        pipe.passed = true
        score++
      }
    }

    expect(score).toBe(1) // 只通过了第一根管道
    expect(pipes[0].passed).toBe(true)
    expect(pipes[1].passed).toBe(false)
  })

  test('同一根管道不会重复计分', () => {
    const birdX = 120
    const pipe = { x: 50, width: 60, passed: true } // 已通过

    let score = 0
    if (!pipe.passed && birdX > pipe.x + pipe.width) {
      pipe.passed = true
      score++
    }

    expect(score).toBe(0) // 不应重复计分
  })
})

用 AI 辅助生成测试用例

对于边界条件复杂的函数,可以让 AI 先生成测试骨架,再人工补充遗漏场景:

markdown
请为以下 TypeScript 函数生成 Jest 单元测试,覆盖正常输入、边界值和异常输入:

```typescript
function calculateScore(combo: number, timeBonus: number): number {
  if (combo < 0 || timeBonus < 0) return 0
  return Math.floor(combo * 100 * (1 + timeBonus / 10))
}
```

AI 通常能覆盖:
- 正常输入(combo=5, timeBonus=10)
- 边界值(combo=0, timeBonus=0)
- 异常输入(负数、极大值)

你需要人工补充的往往是:
- 业务特有的边界(如 combo 上限、timeBonus 精度)
- 与微信小游戏环境相关的测试(如弱网、异常回调)
- 针对历史 Bug 的回归测试

<WmgCalloutBox type="best-practice" title="AI 辅助测试的原则">

1. **让 AI 写骨架,人写业务边界**:AI 不擅长发现你的业务漏洞
2. **核心循环优先覆盖**:得分、碰撞、状态转换比 UI 动画更重要
3. **保留人工审查**:AI 生成的测试可能包含过时的 API 或错误的断言
4. **与 CI 集成**:每次提交自动运行核心逻辑单元测试

</WmgCalloutBox>

---

## 组件测试:Cocos Creator

Cocos Creator 的组件测试需要在引擎运行时环境中执行。推荐使用 Cocos Creator 的测试框架或通过场景快照进行验证。

```typescript
// tests/BirdController.test.ts
// 注意:此测试需要在 Cocos Creator 编辑器环境中运行

import { BirdController } from '../assets/scripts/BirdController'
import { Node, RigidBody2D } from 'cc'

describe('BirdController', () => {
  let birdNode: Node
  let controller: BirdController

  beforeEach(() => {
    // 创建测试节点
    birdNode = new Node('Bird')
    birdNode.addComponent(RigidBody2D)
    controller = birdNode.addComponent(BirdController)
  })

  test('点击屏幕时小鸟向上施加力', () => {
    const rigid = birdNode.getComponent(RigidBody2D)!
    const initialVelY = rigid.linearVelocity.y

    // 模拟触摸事件
    controller.onTouch()

    // 验证向上的力被施加
    expect(rigid.linearVelocity.y).toBeGreaterThan(initialVelY)
  })

  test('小鸟死亡状态下点击不生效', () => {
    // 设置死亡状态
    ;(controller as any).isDead = true

    const rigid = birdNode.getComponent(RigidBody2D)!
    const initialVelY = rigid.linearVelocity.y

    controller.onTouch()

    // 死亡后不应响应点击
    expect(rigid.linearVelocity.y).toBe(initialVelY)
  })
})

E2E 测试:微信小游戏自动化

微信小游戏的 E2E 测试主要在真机上进行,使用微信官方或第三方自动化框架。

框架选择
  • minium(Python):微信早期官方自动化框架,功能完善但维护频率降低。
  • miniprogram-automator(Node.js):微信官方更新的自动化方案,对 JS/TS 开发者更友好,推荐新项目优先考虑。

两者原理相似,本章以 minium 为例讲解核心概念,Node.js 版本使用方法可参考 miniprogram-automator 文档

minium(微信官方)集成

python
# tests/e2e/test_game_basic.py
import minium

class TestGameBasic(minium.MiniTest):
    def test_game_launch(self):
        """测试游戏正常启动"""
        # 等待游戏加载完成
        self.app.wait_for(5)

        # 获取当前页面信息
        page = self.app.get_current_page()
        self.assertIsNotNone(page)

        # 验证 Canvas 元素存在(小游戏的核心渲染层)
        canvas = page.get_element('canvas')
        self.assertIsNotNone(canvas)

    def test_tap_to_start(self):
        """测试点击开始游戏"""
        # 模拟触摸事件
        self.app.tap(200, 400)

        # 等待游戏状态切换
        self.app.wait_for(2)

        # 验证计分 UI 出现(游戏已开始)
        # 通过截图或元素检测

    def test_game_over_scenario(self):
        """测试完整游戏流程:开始 → 碰撞 → 结束"""
        self.app.tap(200, 400)  # 开始游戏

        # 等待管道出现并进行碰撞
        self.app.wait_for(10)

        # 验证 Game Over 界面出现
        # 通过截图像素检测或性能数据变化

    def test_ad_revival(self):
        """测试广告复活流程"""
        self.app.tap(200, 400)  # 开始
        self.app.wait_for(10)   # 等待碰撞

        # 检查复活按钮是否出现
        # 模拟点击复活按钮
        # 验证激励视频广告触发

自动化测试流水线

yaml
# .github/workflows/e2e-test.yml
name: E2E Tests

on:
  pull_request:
    branches: [main]

jobs:
  e2e:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install minium
        run: pip install minium

      - name: Run E2E tests
        run: |
          minitest -m tests/e2e -c config.json

视觉回归测试

游戏的视觉回归测试通过截图对比确保渲染输出正确:

typescript
// tests/visual/visual-regression.test.ts
import { compareScreenshots } from './helpers/pixelmatch-helper'

describe('视觉回归测试', () => {
  test('游戏主界面渲染一致性', async () => {
    // 1. 截取当前版本的屏幕截图
    const currentScreenshot = await captureGameScreen()

    // 2. 加载基准截图(baseline)
    const baselinePath = 'tests/visual/baselines/game-main-scene.png'

    // 3. 像素级对比
    const diff = await compareScreenshots(baselinePath, currentScreenshot, {
      threshold: 0.01, // 允许 1% 的像素差异(抗锯齿导致)
    })

    // 4. 如果差异过大,生成 diff 图片供人工审查
    if (diff.mismatchedPercentage > 0.01) {
      await saveDiffImage(diff, 'tests/visual/diffs/game-main-scene-diff.png')
    }

    expect(diff.mismatchedPercentage).toBeLessThanOrEqual(0.01)
  })

  test('UI 元素在不同分辨率下的渲染', async () => {
    const resolutions = [
      { width: 375, height: 667 }, // iPhone 6/7/8
      { width: 414, height: 896 }, // iPhone 11
      { width: 390, height: 844 }, // iPhone 14/15
      { width: 360, height: 800 }, // 常见安卓
    ]

    for (const res of resolutions) {
      setCanvasSize(res.width, res.height)
      const screenshot = await captureGameScreen()
      const baseline = `tests/visual/baselines/game-${res.width}x${res.height}.png`

      const diff = await compareScreenshots(baseline, screenshot, {
        threshold: 0.02, // 不同分辨率允许略微更大的差异
      })
      expect(diff.mismatchedPercentage).toBeLessThanOrEqual(0.02)
    }
  })
})

性能基准测试

typescript
// tests/perf/performance.test.ts
describe('性能基准测试', () => {
  test('游戏循环在 16ms 内完成(60fps)', () => {
    const iterations = 100
    const frameTimes: number[] = []

    for (let i = 0; i < iterations; i++) {
      const start = performance.now()
      gameManager.update(16.67) // 模拟 16.67ms 帧间隔
      gameManager.render()
      frameTimes.push(performance.now() - start)
    }

    // P99 帧时间不超过 16ms
    frameTimes.sort((a, b) => a - b)
    const p99 = frameTimes[Math.floor(iterations * 0.99)]

    expect(p99).toBeLessThanOrEqual(16)
    // 平均帧时间不超过 8ms(留出渲染余量)
    const avg = frameTimes.reduce((a, b) => a + b, 0) / iterations
    expect(avg).toBeLessThanOrEqual(8)
  })

  test('对象池无内存泄漏', () => {
    const pool = new ObjectPool<TestObj>(() => new TestObj())

    // 模拟 1000 次获取/回收循环
    for (let i = 0; i < 1000; i++) {
      const objs = Array.from({ length: 50 }, () => pool.get())
      objs.forEach((o) => pool.recycle(o))
    }

    // 池中对象数应保持稳定
    expect(pool.size()).toBeLessThanOrEqual(100)
  })

  test('100 个实体同屏时帧率不低于 30fps', async () => {
    // 创建 100 个移动实体
    const entities = Array.from({ length: 100 }, (_, i) => {
      return { x: i * 10, y: 200, vx: 2, vy: 0 }
    })

    const frameCount = 60 // 模拟 60 帧
    const frameTimes: number[] = []

    for (let f = 0; f < frameCount; f++) {
      const start = performance.now()
      for (const entity of entities) {
        entity.x += entity.vx
        entity.y += entity.vy
      }
      frameTimes.push(performance.now() - start)
    }

    const maxFrameTime = Math.max(...frameTimes)
    expect(maxFrameTime).toBeLessThanOrEqual(33) // 30fps = 33.3ms
  })
})

CI 集成测试流水线

yaml
# .github/workflows/game-test.yml
name: Game Tests

on: [push, pull_request]

jobs:
  unit-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }

      - run: pnpm install
      - run: pnpm test -- --coverage

      - name: Check coverage
        run: |
          COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
          if (( $(echo "$COVERAGE < 80" | bc -l) )); then
            echo "Coverage $COVERAGE% is below 80% threshold"
            exit 1
          fi

  perf-benchmark:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }

      - run: pnpm install
      - run: pnpm test:perf

      - name: Compare with baseline
        run: |
          node scripts/compare-perf-baseline.js
          # 如果性能退化超过 5%,CI 失败

  lint-and-typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }

      - run: pnpm install
      - run: pnpm lint
      - run: pnpm typecheck

游戏测试金字塔
        ╱  E2E ╲         ← 少量(核心用户流程,如启动→玩法→游戏结束)
      ╱ 集成测试 ╲       ← 适量(场景切换、引擎 API 交互、模块协作)
    ╱   组件测试   ╲     ← 适量(引擎组件、实体行为、生命周期钩子)
  ╱   单元测试 (大量)   ╲ ← 大量(碰撞检测、计分逻辑、状态机、工具函数)

游戏测试的核心原则:

  1. 逻辑与渲染分离 — 核心游戏逻辑不依赖 Canvas/WebGL,便于单元测试
  2. 输入端可控 — 使用依赖注入替代全局状态(wx.getSystemInfoSync 等)
  3. 帧逻辑可时序化 — 将随机因素(Math.random、物理引擎)通过参数注入控制
  4. 真机测试不可替代 — 桌面环境和真机在性能、输入、渲染方面差异巨大

📚 相关阅读


📝 课后练习

练习 1:为碰撞检测函数编写完整的 Jest 测试用例

题目: 为以下碰撞检测函数编写完整的 Jest 测试套件,覆盖正常情况、边界条件和边缘场景:

typescript
// 圆形与矩形碰撞检测
function circleRectCollision(
  cx: number,
  cy: number,
  radius: number,
  rx: number,
  ry: number,
  rw: number,
  rh: number
): boolean {
  // 找到矩形上离圆心最近的点
  const closestX = Math.max(rx, Math.min(cx, rx + rw))
  const closestY = Math.max(ry, Math.min(cy, ry + rh))

  const distX = cx - closestX
  const distY = cy - closestY

  return distX * distX + distY * distY < radius * radius
}

要求覆盖以下场景:

  1. 圆完全在矩形内部
  2. 圆与矩形边缘重叠
  3. 圆与矩形完全分离
  4. 圆与矩形顶点接触(接触点不在边上)
  5. 矩形在圆内部
  6. 半径为 0 的退化圆
  7. 宽度/高度为 0 的退化矩形

参考答案:

typescript
describe('圆形与矩形碰撞检测 (circleRectCollision)', () => {
  // 1. 圆完全在矩形内部
  test('圆完全在矩形内部时返回 true', () => {
    // 矩形 (0,0,100,100),圆心 (50,50) 半径 20 — 圆完全在矩形内
    expect(circleRectCollision(50, 50, 20, 0, 0, 100, 100)).toBe(true)
  })

  // 2. 圆与矩形边缘重叠
  test('圆与矩形右边重叠时返回 true', () => {
    // 圆心在矩形右侧边缘外侧但半径覆盖到边缘
    expect(circleRectCollision(110, 50, 15, 0, 0, 100, 100)).toBe(true)
  })

  // 3. 圆与矩形完全分离
  test('圆与矩形完全分离时返回 false', () => {
    expect(circleRectCollision(200, 200, 10, 0, 0, 100, 100)).toBe(false)
  })

  // 4. 圆与矩形顶点接触
  test('圆与矩形右上角顶点重叠时返回 true', () => {
    // 矩形的右上角在 (100, 0),圆心在 (100, -5),半径 10
    expect(circleRectCollision(100, -5, 10, 0, 0, 100, 100)).toBe(true)
  })

  test('圆与矩形顶点刚好错过时返回 false', () => {
    // 圆心到右上角距离 > 半径
    expect(circleRectCollision(115, -5, 10, 0, 0, 100, 100)).toBe(false)
  })

  // 5. 矩形在圆内部
  test('矩形完全在圆内部时返回 true', () => {
    expect(circleRectCollision(50, 50, 100, 30, 30, 40, 40)).toBe(true)
  })

  // 6. 退化圆 (radius = 0)
  test('半径为 0 的圆视为一个点', () => {
    // 点在矩形内部
    expect(circleRectCollision(50, 50, 0, 0, 0, 100, 100)).toBe(true)
    // 点在矩形外部
    expect(circleRectCollision(200, 200, 0, 0, 0, 100, 100)).toBe(false)
    // 点在矩形边上(边界情况)
    expect(circleRectCollision(0, 50, 0, 0, 0, 100, 100)).toBe(true)
  })

  // 7. 退化矩形 (宽度/高度为 0)
  test('宽度为 0 的矩形退化为线段', () => {
    // 矩形退化为 x=50 上的竖线段
    expect(circleRectCollision(50, 50, 10, 50, 0, 0, 100)).toBe(true)
    expect(circleRectCollision(70, 50, 10, 50, 0, 0, 100)).toBe(false)
  })

  test('高度为 0 的矩形退化为线段', () => {
    expect(circleRectCollision(50, 50, 10, 0, 50, 100, 0)).toBe(true)
    expect(circleRectCollision(50, 70, 10, 0, 50, 100, 0)).toBe(false)
  })

  // 边界:刚好接触到边缘
  test('圆与矩形边缘刚好接触(无重叠)时返回 false', () => {
    // 距离恰好等于半径
    expect(circleRectCollision(110, 50, 10, 0, 0, 100, 100)).toBe(false)
  })
})
练习 2:设计游戏性能基准测试方案

题目: 为你的小游戏设计一套性能基准测试方案,确保关键场景下的帧率不低于 30fps。要求:

  1. 选取 3 个有代表性的测试场景(如:正常游玩、大量实体、特效爆发)
  2. 定义每个场景的测试参数和通过标准
  3. 实现自动化测试脚本
  4. 设计 CI 中的性能回归检测机制

参考答案:

typescript
// tests/perf/scenario-benchmarks.test.ts

interface BenchmarkScenario {
  name: string
  setup: () => void
  run: () => void
  duration: number // 测试持续时间 (ms)
  maxP99FrameTime: number // P99 帧时间阈值 (ms)
  maxMemoryGrowth: number // 内存增量阈值 (MB)
}

class PerformanceBenchmarkRunner {
  private frameTimes: number[] = []
  private startMemory = 0

  /** 运行单个场景的性能基准测试 */
  async runScenario(scenario: BenchmarkScenario): Promise<{
    passed: boolean
    p99: number
    avgFps: number
    memoryGrowth: number
  }> {
    console.log(`\n🏃 运行场景: ${scenario.name}`)

    scenario.setup()
    this.frameTimes = []
    this.startMemory = this.getCurrentMemory()

    const startTime = performance.now()

    // 运行场景
    await new Promise<void>((resolve) => {
      const loop = () => {
        if (performance.now() - startTime >= scenario.duration) {
          resolve()
          return
        }
        const frameStart = performance.now()
        scenario.run()
        this.frameTimes.push(performance.now() - frameStart)
        requestAnimationFrame(loop)
      }
      requestAnimationFrame(loop)
    })

    // 计算指标
    const p99 = this.calculateP99()
    const avgFps = 1000 / (this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length)
    const endMemory = this.getCurrentMemory()
    const memoryGrowth = endMemory - this.startMemory

    const passed = p99 <= scenario.maxP99FrameTime && memoryGrowth <= scenario.maxMemoryGrowth

    console.log(`  P99: ${p99.toFixed(1)}ms (阈值: ${scenario.maxP99FrameTime}ms)`)
    console.log(`  平均 FPS: ${avgFps.toFixed(1)}`)
    console.log(`  内存增长: ${memoryGrowth.toFixed(1)}MB (阈值: ${scenario.maxMemoryGrowth}MB)`)
    console.log(`  结果: ${passed ? '✅ 通过' : '❌ 未通过'}`)

    return { passed, p99, avgFps, memoryGrowth }
  }

  private calculateP99(): number {
    const sorted = [...this.frameTimes].sort((a, b) => a - b)
    const idx = Math.floor(sorted.length * 0.99)
    return sorted[idx]
  }

  /** 获取当前内存使用量(MB),仅 Chrome/V8 环境可用 */
  private getCurrentMemory(): number {
    try {
      const perf = (performance as any).memory
      return perf?.usedJSHeapSize ? perf.usedJSHeapSize / 1024 / 1024 : 0
    } catch {
      // 非 V8 环境(如 iOS JavaScriptCore)不支持 performance.memory
      return 0
    }
  }
}

// ===== 定义测试场景 =====

const scenarios: BenchmarkScenario[] = [
  {
    name: '正常游玩 — 60fps 稳定',
    setup: () => {
      // 初始化标准游戏场景
      gameManager.init()
      gameManager.start()
    },
    run: () => {
      gameManager.update(16.67)
      gameManager.render()
    },
    duration: 10000, // 测试 10 秒
    maxP99FrameTime: 18, // 60fps = 16.67ms,允许 18ms
    maxMemoryGrowth: 10, // 允许 10MB 以内的内存增长
  },
  {
    name: '高负载 — 200 个实体同屏',
    setup: () => {
      gameManager.init()
      // 创建 200 个实体
      for (let i = 0; i < 200; i++) {
        gameManager.spawnEntity({ x: Math.random() * 750, y: Math.random() * 1334 })
      }
      gameManager.start()
    },
    run: () => {
      gameManager.update(16.67)
      gameManager.render()
    },
    duration: 5000,
    maxP99FrameTime: 33, // 30fps = 33.3ms
    maxMemoryGrowth: 20,
  },
  {
    name: '特效爆发 — 连续创建粒子',
    setup: () => {
      gameManager.init()
      gameManager.start()
    },
    run: () => {
      // 每帧创建 10 个粒子
      for (let i = 0; i < 10; i++) {
        gameManager.spawnParticle({ x: 200, y: 400 })
      }
      gameManager.update(16.67)
      gameManager.render()
    },
    duration: 5000,
    maxP99FrameTime: 33,
    maxMemoryGrowth: 15,
  },
]

// ===== 运行基准测试 =====
describe('性能基准测试', () => {
  const runner = new PerformanceBenchmarkRunner()

  for (const scenario of scenarios) {
    test(
      scenario.name,
      async () => {
        const result = await runner.runScenario(scenario)
        expect(result.passed).toBe(true)
      },
      30000
    ) // 单场景超时 30 秒
  }
})

CI 性能回归检测配置 (scripts/compare-perf-baseline.js):

javascript
// scripts/compare-perf-baseline.js
const fs = require('fs')
const path = require('path')

const BASELINE_PATH = path.join(__dirname, '..', 'tests', 'perf', 'baseline.json')
const CURRENT_PATH = path.join(__dirname, '..', 'tests', 'perf', 'current.json')

function compare() {
  const baseline = JSON.parse(fs.readFileSync(BASELINE_PATH, 'utf-8'))
  const current = JSON.parse(fs.readFileSync(CURRENT_PATH, 'utf-8'))

  let hasRegression = false

  for (const [scenario, metrics] of Object.entries(current)) {
    const base = baseline[scenario]
    if (!base) continue

    const p99Degradation = ((metrics.p99 - base.p99) / base.p99) * 100
    const memDegradation = ((metrics.memoryGrowth - base.memoryGrowth) / base.memoryGrowth) * 100

    console.log(`\n📊 ${scenario}:`)
    console.log(
      `  P99: ${base.p99.toFixed(1)}ms → ${metrics.p99.toFixed(1)}ms (${p99Degradation > 0 ? '+' : ''}${p99Degradation.toFixed(1)}%)`
    )
    console.log(
      `  内存: ${base.memoryGrowth.toFixed(1)}MB → ${metrics.memoryGrowth.toFixed(1)}MB (${memDegradation > 0 ? '+' : ''}${memDegradation.toFixed(1)}%)`
    )

    if (p99Degradation > 10) {
      console.error(`  ❌ P99 性能退化 ${p99Degradation.toFixed(1)}% 超过 10% 阈值`)
      hasRegression = true
    }
    if (memDegradation > 20) {
      console.error(`  ❌ 内存增长 ${memDegradation.toFixed(1)}% 超过 20% 阈值`)
      hasRegression = true
    }
  }

  if (hasRegression) {
    console.error('\n❌ 性能回归检测未通过,请检查代码变更')
    process.exit(1)
  }

  console.log('\n✅ 性能回归检测通过')
}

compare()

用心学习,持续实践