乐于分享
好东西不私藏

AI 软件测试工程:Lesson 0008:自动化测试入门 — 从手工到代码

AI 软件测试工程:Lesson 0008:自动化测试入门 — 从手工到代码

第四部分:自动化测试 · Level 3 入门

从手工点击到代码验证:写你的第一个自动化测试,理解 AAA 模式,用 Vitest 为 TaskFlow 建立第一道自动防线。


🎯 前 7 节课我们掌握了"测什么、测多少、怎么记、怎么管"。从这节课开始,我们进入自动化测试的世界——用手写代码来执行测试,让机器替我们做重复的事。Lesson 0008 是 Level 3 的起点,目标是:你能为 TaskFlow 写第一个自动化测试,并在 CI/CD 中运行它。

📑 本节目录

  • 学习目标
  • 什么是单元测试
  • 为什么要自动化
  • Vitest 快速上手
  • AAA 模式:写测试的"万能公式"
  • 写第一个测试
  • 运行测试与 CI/CD 集成
  • 核心比喻
  • 示例
  • 实践:TaskFlow 第一个自动化测试
  • AI 实践
  • QA Checklist
  • 思考题
  • 推荐阅读

📌 学习目标

为什么学习

手工测试的问题是:每次代码变更都要重新测。一个功能改了 5 行代码,你需要手动回归 20 个用例;每次发布前要跑 3 小时手工测试。自动化测试让你写一次,跑一万次,而且每次都一样快、一样准确。

你将掌握:

  • 理解什么是单元测试、集成测试和 E2E 测试(定位在 Trophy 模型中)
  • 掌握 Vitest 的安装和基本配置
  • 掌握 AAA 模式(Arrange-Act-Assert)
  • 为 TaskFlow 写第一个自动化测试
  • 将测试接入 GitHub Actions CI/CD

实际价值

  • 每次代码变更,自动运行测试 → 发现问题立即知道
  • 发布前自动回归 → 不再有"上线前夜的恐慌"
  • 代码审查时,测试是"安全网" → 大胆重构
  • AI Coding 时代,AI 生成代码必须由自动化测试验证

AI 时代如何应用

AI Coding 工具(Claude Code、GitHub Copilot)每秒生成大量代码——这些代码必须由自动化测试验证。没有测试的 AI 生成代码 = 随时可能崩溃的地雷。测试工程师的职责变成:设计测试、审查 AI 生成的测试、执行人类判断。


📚 什么是单元测试?

**单元测试(Unit Test)**是测试的"原子"——它测试代码中最小的可测试单元(通常是单个函数或方法),完全隔离于外部依赖(数据库、API、文件系统)。

单元测试的 3 个核心特征

特征
含义
TaskFlow 示例
快速
毫秒级执行(不涉及真实数据库)
测试 100 个用例只需 3 秒
隔离
Mock 掉所有外部依赖
测试日期计算函数,不连接 Supabase
确定性
每次运行结果一致(无 flaky)
同样的输入,永远得到同样的输出

单元测试 vs 集成测试 vs E2E 测试

回顾 Lesson 0005 的 Trophy 模型,自动化测试分为 3 个层次:

层次
测什么
外部依赖
速度
找到 Bug 的精准度
单元测试
单个函数/方法
Mock 掉全部
毫秒
高(定位精准)
集成测试
组件协作(API + DB)
部分 Mock
秒级
E2E 测试
完整用户路径
全部真实
分钟级
低(只知道出事了)

💡 记住:单元测试不是越多越好,而是越有价值越好。测试一个无意义的函数 100 次,不如测试一个关键函数 3 次。


🤔 为什么要自动化?

💡 比喻 · 手工洗碗 vs 洗碗机

手工测试就像每次吃完饭用手洗碗:

  • 每次都要洗(每次代码变更都要重新测试)
  • 速度慢(手工测试 1 小时,自动化 10 秒)
  • 会累(重复 100 次后,人会出错、疲惫)
  • 别人无法代劳(只有你知道怎么洗这个碗)

自动化测试就像买洗碗机:

  • 一次性投入(写测试要时间)
  • 长期受益(之后每次都自动运行)
  • 速度极快(每次几分钟)
  • 结果一致(机器不会累)

洗碗机的成本是买机器的钱;自动化测试的成本是写测试的时间。但一旦写好,长期免费

手工测试 vs 自动化测试对比

维度
手工测试
自动化测试
执行
人工操作
机器执行
速度
慢(分钟/小时)
快(毫秒/秒)
准确性
人会出错
机器无误
重复成本
每次都要人工
几乎为零
初始成本
低(直接测)
高(需要写代码)
长期成本
高(持续人工)
低(维护量小)
适用场景
探索性测试、一次性测试
回归测试、频繁变更的代码
适合人员
测试工程师(手工)
开发工程师(自动化)

什么时候应该自动化?

不是所有测试都值得自动化。以下是决策标准:

应该自动化的 ✅
不应该自动化的 ❌
回归测试(每次发布都要跑)
一次性测试(只跑一次)
核心功能的冒烟测试
探索性测试(没有固定步骤)
频繁变更的代码逻辑
UI 细节测试(颜色、字体)
需要多种数据组合的测试
难以自动化的复杂交互
API 响应验证
极端边界情况(人工更灵活)

⚡ Vitest 快速上手

Vitest是 Next.js + TypeScript 项目的首选测试框架——它比 Jest 更快(基于 Vite),与 Next.js 生态无缝集成,API 与 Jest 兼容。

Vitest vs Jest vs Mocha

维度
Vitest
Jest
Mocha
速度
极快(Vite 引擎)
中等
中等
TypeScript
原生支持
需配置
需配置
Next.js 兼容
✅ 原生
✅ 可以
⚠️ 需要适配
HMR(热更新)
✅ 支持
❌ 不支持
❌ 不支持
学习曲线
低(Jest 兼容)
推荐场景
React/Vue + Vite 项目
通用 JS 项目
Node.js 服务

安装 Vitest

# 在 taskflow 项目中安装
cd taskflow

# 安装 Vitest 核心
npm install -D vitest

# 安装 React Testing Library
npm install -D @testing-library/react @testing-library/user-event
npm install -D @testing-library/jest-dom

# 安装 JSdom(模拟浏览器环境)
npm install -D jsdom

# 安装 TypeScript 类型
npm install -D @types/node

配置 vitest.config.ts

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
plugins: [react()],
test: {
environment'jsdom',       // 模拟浏览器 DOM
setupFiles: ['./src/test/setup.ts'], // 测试初始化文件
globalstrue,              // 全局 test/it/expect
include: ['src/**/*.{test,spec}.{js,ts}'],
exclude: ['node_modules''dist'],
  },
})

配置 package.json scripts

// 在 package.json 中添加
{
"scripts"{
"test""vitest",
"test:run""vitest run",// 单次运行(CI/CD 用)
"test:ui""vitest --ui",// 可视化 UI
"test:coverage""vitest run --coverage"// 覆盖率报告
}
}

创建测试初始化文件

// src/test/setup.ts
import '@testing-library/jest-dom'
// 配置测试环境所需的全局 Mock

📐 AAA 模式:写测试的"万能公式"

**AAA 模式(Arrange-Act-Assert)**是写自动化测试的"万能公式"。无论你测什么,都遵循这 3 个步骤。

AAA 是什么?

阶段
中文
做什么
Arrange
准备
设置输入数据、Mock 依赖、准备被测对象
Act
执行
调用被测函数/方法
Assert
断言
验证结果是否符合预期

📝 示例:测试 TaskFlow 的日期格式化函数

// src/utils/dateUtils.test.ts
import { describe, it, expect } from 'vitest'
import { formatDueDate } from '../dateUtils'

describe('formatDueDate'() => {

// ===== Arrange =====
// 准备阶段:准备好各种输入数据

// ===== Act =====
// 执行阶段:调用被测函数

// ===== Assert =====
// 断言阶段:验证结果

it('显示"今天"当截止日期是今天'() => {
// Arrange
const today = new Date('2026-07-20')

// Act
const result = formatDueDate(today)

// Assert
expect(result).toBe('今天')
})

it('显示"明天"当截止日期是明天'() => {
// Arrange
const tomorrow = new Date('2026-07-21')

// Act
const result = formatDueDate(tomorrow)

// Assert
expect(result).toBe('明天')
})

it('显示完整日期当截止日期超过 1 天后'() => {
// Arrange
const future = new Date('2026-08-01')

// Act
const result = formatDueDate(future)

// Assert
expect(result).toBe('2026-08-01')
})

it('显示"已过期"当截止日期在过去'() => {
// Arrange
const past = new Date('2026-07-01')

// Act
const result = formatDueDate(past)

// Assert
expect(result).toBe('已过期')
})
})

AAA 模式为什么重要?

  1. 结构清晰:读测试的人立即知道"准备什么 → 做什么 → 验证什么"
  2. 易于调试:如果测试失败,你立即知道是哪个阶段出了问题
  3. 易于维护:添加新用例只需复制一个 AAA 块,改 Arrange 和 Assert
  4. 防止遗漏:每个测试都必须有这三个部分,强制你完整思考

AAA 的常见错误

❌ 错误 1:Act 和 Assert 混在一起

// 差:Act 和 Assert 混在一行
expect(formatDueDate(new Date('2026-07-20'))).toBe('今天')

// 好:分开
const result = formatDueDate(new Date('2026-07-20'))
expect(result).toBe('今天')

❌ 错误 2:Arrange 包含太多东西

// 差:一个测试里设置了一堆无关的东西
it('格式化日期'() => {
const db = new MockDatabase()   // 无关的数据库
const user = { id1name'Test' }  // 无关的用户
const config = { timezone'Asia/Shanghai' }  // 无关的配置
const date = new Date('2026-07-20')
// ...
})

✍️ 写第一个测试:TaskFlow 日期工具函数

让我们为 TaskFlow 的实际代码写测试。先看一下我们的代码结构:

TaskFlow 工具函数示例

// src/utils/dateUtils.ts
// 这是我们要测试的函数

export function formatDueDate(dateDate): string {
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const target = new Date(date.getFullYear(), date.getMonth(), date.getDate())
const diffDays = Math.floor((target.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))

if (diffDays === 0return '今天'
if (diffDays === 1return '明天'
if (diffDays < 0return '已过期'
return date.toISOString().split('T')[0]
}

export function validateEmail(emailstring): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}

export function validateTaskTitle(titlestring): { validbooleanerror?: string } {
if (!title || title.trim() === '') {
return { validfalseerror'标题不能为空' }
  }
if (title.length < 3) {
return { validfalseerror'标题至少 3 个字符' }
  }
if (title.length > 50) {
return { validfalseerror'标题最多 50 个字符' }
  }
return { validtrue }
}

第一个测试文件

// src/utils/dateUtils.test.ts
import { describe, it, expect } from 'vitest'
import { formatDueDate } from './dateUtils'

describe('formatDueDate'() => {

// 正常路径
it('今天返回"今天"'() => {
const today = new Date()
const result = formatDueDate(today)
expect(result).toBe('今天')
  })

// 正常路径
it('明天返回"明天"'() => {
const tomorrow = new Date()
    tomorrow.setDate(tomorrow.getDate() + 1)
const result = formatDueDate(tomorrow)
expect(result).toBe('明天')
  })

// 边界值
it('昨天返回"已过期"'() => {
const yesterday = new Date()
    yesterday.setDate(yesterday.getDate() - 1)
const result = formatDueDate(yesterday)
expect(result).toBe('已过期')
  })

// 正常路径
it('超过1天返回日期字符串'() => {
const future = new Date()
    future.setDate(future.getDate() + 5)
const result = formatDueDate(future)
// 格式:YYYY-MM-DD
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/)
  })
})

第二个测试文件:表单验证

// src/utils/validateUtils.test.ts
import { describe, it, expect } from 'vitest'
import { validateEmail, validateTaskTitle } from './validateUtils'

describe('validateEmail'() => {

// 有效等价类
it('标准邮箱格式通过验证'() => {
expect(validateEmail('user@taskflow.com')).toBe(true)
  })

it('带点的邮箱格式通过验证'() => {
expect(validateEmail('first.last@taskflow.com')).toBe(true)
  })

it('带加号的邮箱格式通过验证'() => {
expect(validateEmail('user+tag@taskflow.com')).toBe(true)
  })

// 无效等价类
it('缺少 @ 符号验证失败'() => {
expect(validateEmail('user.taskflow.com')).toBe(false)
  })

it('缺少域名部分验证失败'() => {
expect(validateEmail('user@')).toBe(false)
  })

it('包含空格验证失败'() => {
expect(validateEmail('user @taskflow.com')).toBe(false)
  })
})

describe('validateTaskTitle'() => {

// 有效等价类
it('3-50 字符标题通过验证'() => {
expect(validateTaskTitle('项目启动')).toEqual({ validtrue })
  })

it('恰好 50 字符标题通过验证'() => {
const title50 = 'a'.repeat(50)
expect(validateTaskTitle(title50)).toEqual({ validtrue })
  })

// 边界值
it('2 字符标题验证失败(太短)'() => {
expect(validateTaskTitle('ab')).toEqual({
validfalse,
error'标题至少 3 个字符'
    })
  })

it('51 字符标题验证失败(太长)'() => {
const title51 = 'a'.repeat(51)
expect(validateTaskTitle(title51)).toEqual({
validfalse,
error'标题最多 50 个字符'
    })
  })

// 无效等价类
it('空字符串验证失败'() => {
expect(validateTaskTitle('')).toEqual({
validfalse,
error'标题不能为空'
    })
  })

it('纯空格验证失败'() => {
expect(validateTaskTitle('   ')).toEqual({
validfalse,
error'标题不能为空'
    })
  })
})

🚀 运行测试与 CI/CD 集成

本地运行

# 交互式运行(watch 模式,代码变更自动重跑)
npm test

# 单次运行(CI/CD 用,变更后自动退出)
npm run test:run

# 查看测试覆盖率
npm run test:coverage

# 可视化测试 UI
npm run test:ui

Vitest 输出示例

 RUN  v1.x.x .../taskflow

 ✓ src/utils/dateUtils.test.ts (4 tests)  45ms
 ✓ src/utils/validateUtils.test.ts (10 tests)  32ms

 Test Files  2 passed (2)
 Tests      14 passed (14)  ✓
 Time       1.23s

接入 GitHub Actions CI/CD

// .github/workflows/test.yml
name: Tests

on:
push:
branches: [maindevelop]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

steps:
uses: actions/checkout@v4

name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

name: Install dependencies
run: npm ci

name: Run tests
run: npm run test:run

name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/

测试覆盖率

覆盖率是测试"跑到了多少代码"的量化指标。Vitest 内置 v8 覆盖率工具。

指标
含义
参考目标
行覆盖率
执行到的代码行数 / 总行数
≥ 80%
函数覆盖率
被调用的函数数 / 总函数数
≥ 80%
分支覆盖率
if/else 的每个分支都跑到
≥ 70%
语句覆盖率
每个语句都执行到
≥ 80%

⚠️ 重要提醒:覆盖率是工具,不是目标。100% 覆盖率的烂测试不如 70% 覆盖率的好测试。测关键路径,不要测"凑数"的代码。


💡 核心比喻

💡 比喻 1:AAA = 做实验

AAA 模式就像做科学实验:

  • Arrange(准备) = 准备实验器材、设置对照组(科学家不会在没有设备的情况下做实验)
  • Act(执行) = 进行实验操作(施加刺激、注入试剂)
  • Assert(断言) = 测量结果、与预期对比(记录数据,验证假设)

没有这三步的实验不叫实验,叫瞎试。AAA 让你的测试从"瞎试"变成"科学实验"。

💡 比喻 2:测试覆盖率 = 考试分数

覆盖率就像考试分数:

  • 0-30% = 交白卷(几乎没有测试)
  • 50-70% = 及格线(基本覆盖)
  • 80-90% = 良好(重点模块覆盖)
  • 95%+ = 完美主义(注意:100% 不代表没有 Bug)

但考 60 分不代表你真的学会了——可能是蒙的。测得多不代表测得对。覆盖率是参考,不是圣经

💡 比喻 3:Mock = 替身演员

单元测试需要 Mock(模拟)外部依赖,就像拍电影需要替身演员:

  • 你要拍一个演员跳伞的镜头,但你不想让演员真的从飞机上跳下来 → 找个替身(Mock)
  • 你要测试一个函数调用了 Supabase API,但不想真的连接数据库 → Mock 掉 Supabase
  • 替身(Mock)代替真实的演员(真实数据库),但最终的电影效果看起来一样

Mock 的目的是让你测试"当前这个人的表演",而不是"整个剧组的配合"。Mock = 隔离外部依赖,让测试只关心自己的逻辑


📝 示例

📝 示例 1:Vercel 的测试文化

Vercel(Next.js 背后的公司)的工程实践:

  • 每个 PR 必须通过所有测试,否则不能合并
  • 使用 Vitest(因为 Next.js 团队推荐)
  • 测试覆盖率目标:核心模块 ≥ 90%,一般模块 ≥ 70%
  • 使用 Codecov 监控覆盖率趋势,不允许覆盖率下降
  • AI Coding(Copilot)生成的代码必须有对应的测试

📝 反例:覆盖率达标,质量仍然差

某团队覆盖率 95%,但上线后 Bug 不断:

  • ❌ 95% 覆盖率都是"测试函数返回 true"——没有边界值测试
  • ❌ 测试都是"assert(true === true)"——没有真正的逻辑验证
  • ❌ Mock 掉了所有真实的逻辑——测试通过但实际跑不通
  • ❌ 只测试了 happy path——错误处理一个都没测

教训:覆盖的是代码行,不是质量。好的测试测的是行为,不是实现


🛠️ 实践:TaskFlow 第一个自动化测试

步骤

1️⃣ 在 taskflow 项目中安装 Vitest

cd taskflow
npm install -D vitest @testing-library/react @testing-library/user-event
npm install -D @testing-library/jest-dom jsdom
npm install -D @types/node

2️⃣ 创建 vitest.config.ts

// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
environment'jsdom',
setupFiles: ['./src/test/setup.ts'],
globalstrue,
include: ['src/**/*.{test,spec}.{js,ts}'],
  },
})

3️⃣ 创建 src/utils 目录和工具函数

mkdir -p src/utils src/test
touch src/utils/dateUtils.ts
touch src/utils/validateUtils.ts

4️⃣ 创建 src/test/setup.ts

// src/test/setup.ts
import '@testing-library/jest-dom'

5️⃣ 实现 dateUtils.ts

// src/utils/dateUtils.ts
export function formatDueDate(dateDate): string {
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const target = new Date(date.getFullYear(), date.getMonth(), date.getDate())
const diffDays = Math.floor((target.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
if (diffDays === 0return '今天'
if (diffDays === 1return '明天'
if (diffDays < 0return '已过期'
return date.toISOString().split('T')[0]
}

6️⃣ 写 dateUtils.test.ts

// src/utils/dateUtils.test.ts
import { describe, it, expect } from 'vitest'
import { formatDueDate } from './dateUtils'

describe('formatDueDate'() => {
it('今天返回"今天"'() => {
const today = new Date()
expect(formatDueDate(today)).toBe('今天')
  })

it('明天返回"明天"'() => {
const tomorrow = new Date()
    tomorrow.setDate(tomorrow.getDate() + 1)
expect(formatDueDate(tomorrow)).toBe('明天')
  })

it('昨天返回"已过期"'() => {
const yesterday = new Date()
    yesterday.setDate(yesterday.getDate() - 1)
expect(formatDueDate(yesterday)).toBe('已过期')
  })
})

7️⃣ 运行测试

npm run test:run

应该看到:3 passed (3)

8️⃣ 提交代码

git add .
git commit -m "Lesson 0008: TaskFlow 第一个自动化测试(Vitest + AAA)"

🤖 AI 实践

🤖 AI 实践 1:用 AI 从函数生成测试用例

Prompt 模板

请为以下函数生成 Vitest 测试用例,使用 AAA 模式。

```typescript
export function validatePassword(password: string): {
  valid: boolean
  errors: string[]
} {
  const errors: string[] = []
  if (password.length < 8) errors.push('密码至少 8 个字符')
  if (!/[A-Z]/.test(password)) errors.push('密码必须包含大写字母')
  if (!/[a-z]/.test(password)) errors.push('密码必须包含小写字母')
  if (!/[0-9]/.test(password)) errors.push('密码必须包含数字')
  return { valid: errors.length === 0, errors }
}

请生成:

  1. 等价类划分(有效/无效)
  2. 边界值测试(8字符、各种组合)
  3. 每个测试用例的 Arrange-Act-Assert
  4. 格式:Vitest + expect
**预期效果**:得到可以直接运行的 Vitest 测试代码。

### 🤖 AI 实践 2:用 AI 检查现有测试的质量

**Prompt 模板**:

```markdown
请审查以下 Vitest 测试用例的质量,满分 10 分。

```typescript
it('验证邮箱', () => {
  expect(validateEmail('test@test.com')).toBe(true)
})

请分析:

  1. 这个测试覆盖了哪些场景?遗漏了哪些?
  2. 是否有边界值测试?
  3. 是否有无效等价类的测试?
  4. 如何改进?
  5. 建议添加哪些测试用例?
**预期效果**:得到测试质量评估和改进建议。

### 🤖 AI 实践 3:用 AI 写 React Hook 测试

**Prompt 模板**:

```markdown
请为以下 React Hook 生成 Vitest + Testing Library 测试用例。

```typescript
// src/hooks/useTaskFilter.ts
import { useState } from 'react'
import type { Task } from '../types'

export function useTaskFilter(tasks: Task[]) {
  const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all')

  const filteredTasks = tasks.filter(task => {
if (filter === 'active'return !task.completed
if (filter === 'completed'return task.completed
return true
  })

return { filteredTasks, filter, setFilter }
}

请生成:

  1. 测试默认过滤为 'all'
  2. 测试切换到 'active' 只显示未完成任务
  3. 测试切换到 'completed' 只显示已完成任务
  4. 使用 renderHook from @testing-library/react-hooks
**预期效果**:得到完整的 React Hook 测试代码。

---

## ✅ QA Checklist

> 学完这节课,你应该能:

- [ ] 解释什么是单元测试及其 3 个核心特征
- [ ] 区分单元测试、集成测试、E2E 测试的定位
- [ ] 解释 AAA 模式的三个阶段
- [ ] 安装和配置 Vitest
- [ ] 用 describe + it + expect 写测试
- [ ] 为日期格式化函数写测试
- [ ] 为表单验证函数写测试(含边界值)
- [ ] 运行 Vitest 并解读输出
- [ ] 将测试接入 GitHub Actions CI/CD
- [ ] 用 AI 辅助生成测试用例

---

## ❓ 思考题

> 请认真思考这些问题

1. AAA 模式看起来很简单,但为什么很多新手写测试时仍然会写成一团糟?你觉得最难坚持的是哪个 A?
2. 覆盖率 100% 的测试就是"好测试"吗?什么样的测试是"高质量"的测试?
3. 手工测试和自动化测试哪个更"有价值"?为什么有些团队过度追求自动化而忽视了手工探索性测试?
4. 在 AI Coding 时代,AI 可以自动生成测试——这会降低测试工程师的门槛吗?还是说反而提高了对测试设计能力的要求?
5. Vitest 的 watch 模式让你在保存代码时自动重跑测试——这对开发体验有什么影响?有没有潜在的负面影响?
6. Mock 可以隔离外部依赖,但如果 Mock 得太多,测试是否反而失去了意义?如何在"隔离""真实"之间找到平衡?

---

## 📖 推荐阅读

### 文章

- **[Vitest Guide](https://vitest.dev/guide/testing-types.html)** — Vitest 官方
  - Vitest 官方指南,包含完整 API 文档。
- **[Arrange Act Assert Pattern](https://kentcdodds.com/blog/arrange-act-assert)** — Kent C. Dodds
  - AAA 模式的原作者解读。必读。
- **[JavaScript Testing Best Practices](https://github.com/goldbergyoni/javascript-testing-best-practices)** — Yoni Goldberg
  - JavaScript 测试的全面指南,包含单元测试、集成测试、E2E。

### 视频

- **"Vitest Tutorial for Beginners"** — YouTube 搜索
  - 完整的 Vitest 入门教程。
- **"Arrange Act Assert Pattern Explained"** — YouTube 搜索
  - AAA 模式的实操演示。

---

## 💬 任何不清楚的地方?

> 这是你的老师(AI Agent)。Vitest 配置问题、测试写不出来的场景、想用 AI 生成测试的代码——都可以问。

---

## ⏭️ 下一节预告

**Lesson 0009:Mock 与 Stub — 隔离外部依赖的艺术**

Lesson 0008 我们理解了"怎么写测试"。下一节课我们回答"测什么要 Mock""怎么 Mock"

- Mock vs Stub vs Spy 的区别
- 何时应该 Mock(何时不应该)
- Vitest Mock API 详解
- Mock Supabase Auth 和 Database
- TaskFlow 集成测试实战

**前置知识**:Lesson 0008(自动化测试入门 + AAA 模式)

---

> 📅 TaskFlow 测试课程 · Lesson 0008 · Level 3 入门
>
> 设计师:AI 测试架构师 · 持续优化中