⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

9.3 自动化工作流

本节目标:配置 Git Hooks 和 GitHub Actions,让测试自动运行;了解 TDD 的思路和适用场景。


有了测试,但忘了跑

小明花了一个下午给核心接口写了测试,跑了一遍全绿,很有成就感。然后他继续开发新功能——加了电影短评、改了搜索排序、优化了详情页加载。每次改完代码,他都想着"等会儿跑一下测试",但每次都被下一个功能吸引过去,忘了。

三天后他把代码推到 GitHub,朋友拉下来一跑——两个测试挂了。小明回头查,发现是第一天改搜索排序的时候引入的 bug。如果他当天就跑了测试,当场就能发现;拖了三天,中间又改了一堆代码,现在要从三天的改动里找出是哪次提交引入的问题,排查成本翻了好几倍。

这个故事揭示了一个规律:bug 发现得越晚,修复成本越高。改完代码当场发现,你还记得刚才改了什么,修起来几分钟的事。拖到第二天,你得回忆昨天的改动。拖到一周后,你可能已经完全忘了当时的上下文,得重新理解代码才能修。自动化测试的核心价值不是"帮你找 bug"——你手动测也能找到 bug——而是帮你尽早找到 bug,在修复成本最低的时候。

老师傅说:"人会忘,机器不会。让机器帮你记住。"

Git Hooks——提交前自动检查

Git 有一个内置机制叫 Hooks(钩子)——你可以配置一些脚本,让 Git 在特定操作时自动执行。比如 pre-commit hook 会在每次 git commit 之前自动运行,如果脚本执行失败(返回非零退出码),Git 就会阻止这次提交。

这就像门禁系统:你刷卡进门,门禁自动检查你有没有权限,不用你自己记住"要检查"。小明每次 git commit,Git 自动跑 pnpm test——测试全过,提交成功;有测试失败,提交被阻止,他必须先修好再提交。

这个机制的心理效果比技术效果更重要。小明以前总想着"等会儿跑测试",但"等会儿"永远不会来。有了 pre-commit hook,他不需要"记住"跑测试——提交的动作本身就触发了测试。就像你不需要"记住"锁门,因为门禁系统会自动锁。把"应该做但容易忘"的事情变成"自动发生"的事情,是自动化的核心价值。

配置 Git Hooks 最常用的工具是 Husky。它帮你管理 hooks 脚本,不用手动去 .git/hooks/ 目录里折腾。告诉 Claude Code 你的需求,它会帮你配好:

跟 AI 说:"帮我配置 Git pre-commit hook,在每次 commit 前自动运行 pnpm test。测试失败就阻止提交。用 Husky。"

好奇的话展开看看:Husky 配置长什么样
bash
# 安装 Husky
pnpm add -D husky

# 初始化
npx husky init

# .husky/pre-commit 文件内容
pnpm test

就这么简单。以后每次 git commit,Husky 会自动在提交前运行 pnpm test。测试失败,提交被阻止;测试通过,正常提交。

如果你只想跑和本次修改相关的测试(而不是全部测试),可以配合 lint-staged 使用——但对于小明这个规模的项目,跑全部测试也就几秒钟,没必要优化。

但 pre-commit hook 有一个局限:它只在本地运行。如果有人用 git commit --no-verify 跳过了 hook(比如赶时间想先提交再说),坏代码还是能提交上去。而且不同人的本地环境可能不一样——你的电脑上测试全过,换一台电脑可能就挂了(比如依赖了本地才有的环境变量、缓存了旧的构建产物)。所以 Git Hooks 是第一道防线,但不是唯一的防线。

小明可能会问:"那 --no-verify 什么时候该用?"实际开发中有几个合理的场景:WIP 提交(代码还没写完,只是保存中间状态)、纯文档或注释的改动(跑测试没意义)、hook 检查的东西跟本次改动明显无关的时候。关键是有意识地跳过,而不是养成"每次都跳过"的习惯——如果你发现自己经常跳过 hook,那可能是 hook 配置得太重了(比如每次提交都跑全量测试,其实只需要跑相关的),应该优化 hook 本身而不是绕过它。

GitHub Actions——推送后自动验证

代码推到 GitHub 后,GitHub Actions 会在一个全新的、干净的虚拟机上自动运行你的测试。这是第二道防线——即使本地跳过了 hook,CI 会拦住。

这就像工厂的质检流水线:每个产品下线前都要过检测仪,不合格的不允许出厂。工人可能偷懒跳过自检(本地 hook),但流水线上的检测仪不会放过任何一个(CI)。

GitHub Actions 的核心价值不只是"再跑一遍测试"。它在一个干净的环境里从零开始——安装依赖、构建项目、运行测试。这意味着它能发现"在我电脑上能跑"但实际有问题的情况:你本地有一个环境变量但忘了加到 .env.example 里,你本地缓存了旧的构建产物导致测试意外通过,你本地安装了某个全局工具但项目的 package.json 里没声明依赖。这些问题在本地发现不了,但 CI 的干净环境会暴露它们。

跟 AI 说:"帮我写一个 GitHub Actions workflow,在每次 push 和 PR 时自动运行测试。"

好奇的话展开看看:GitHub Actions workflow 文件
yaml
# .github/workflows/test.yml
name: Test

on:
  push:
    branches: [main]
  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

      - name: Setup pnpm
        uses: pnpm/action-setup@v4

      - name: Install dependencies
        run: pnpm install

      - name: Run tests
        run: pnpm test

每次你 push 代码或创建 PR,GitHub 会自动启动一个虚拟机,按照这个流程跑一遍。结果会显示在 PR 页面上——绿色勾表示通过,红色叉表示失败。

两道防线配合使用:Git Hooks 在本地拦截明显的问题(快速反馈,几秒钟),GitHub Actions 在远端做最终验证(全面检查,可能几分钟)。大部分问题在本地就被拦住了,只有少数"环境相关"的问题需要 CI 来发现。

小明第一次看到 GitHub Actions 的绿色勾出现在自己的 PR 页面上时,有一种踏实感——不是"我觉得代码没问题",而是"一个干净环境从零验证过代码没问题"。这种确定性是本地测试给不了的,因为本地环境总有各种"只在我电脑上"的特殊条件。

还有一个实用的技巧:如果 CI 上的测试失败了但本地通过,不要急着在本地反复跑。先看 CI 的日志——它会告诉你具体哪个测试失败了、错误信息是什么。常见的原因是环境变量没配(CI 上没有 .env 文件,需要在 GitHub 仓库的 Settings → Secrets 里配置)、数据库连接失败(CI 上没有本地的数据库实例)、或者时区差异(CI 默认 UTC 时区,你本地可能是 UTC+8)。这些都是"环境问题"而不是"代码问题",解决方法是让 CI 环境和本地环境保持一致。

Git Hooks 位置

git add
pre-commitlint + format
git commit
commit-msg检查格式
git push
pre-push运行测试

GitHub Actions 流水线

Push/PR
安装依赖
Lint 检查
类型检查
单元测试
构建
⚙️部署(可选)

TDD 循环

🔴Red写失败的测试
🟢Green写最少代码通过
🔵Refactor重构优化

TDD——先想清楚再动手

小明要给"个人豆瓣"加一个新功能:电影短评。用户可以给电影写一段文字评论,最多 500 字。老师傅建议:"这次试试先写测试。"

小明困惑:"代码都没写,怎么测?"

老师傅说:"你先想清楚这个接口应该接收什么、返回什么——这就是测试。"

小明想了想:POST /api/movies/42/reviews 接收 { content: "很好看,特效震撼" },返回 201 和评论对象。未登录返回 401。内容为空返回 400。内容超过 500 字返回 400

他把这些写成测试代码。测试当然全部失败了——接口还不存在。然后他告诉 Claude Code:"这是我写好的测试文件,帮我实现对应的 API,让所有测试通过。"

写完他发现一个有趣的事:因为先想清楚了接口设计(接收什么参数、返回什么状态码、拒绝什么输入),代码写起来特别顺畅。不用边写边改接口,不用写到一半发现"这个参数应该叫别的名字"。测试文件本身就是一份接口规格说明——它精确地描述了接口的行为,比口头描述或文档更准确,因为它是可执行的。如果接口的行为和测试描述的不一致,测试会立刻报红。

这就是 TDD(测试驱动开发) 的核心思路:先写测试(定义"什么是正确的"),再写代码(让测试通过),最后重构(优化代码,保持测试通过)。就像先画图纸再盖房子——不会先砌墙再想门开在哪。

TDD 的三个步骤有专门的名字:Red(写测试,测试失败,因为代码还不存在)→ Green(写最少的代码让测试通过)→ Refactor(优化代码结构,保持测试通过)。"最少的代码"这个要求很重要——它防止你过度设计。你不需要一开始就考虑"如果以后要支持半星评分怎么办",先让当前的测试通过就行。以后需求变了,先改测试(定义新的"正确"),再改代码。

但 TDD 不是教条。它最适合的场景是:核心业务逻辑(评分计算、权限判断——算错了后果严重)、需要稳定接口的模块(API 端点——接口设计确定了,后面不容易改)、复杂的条件分支(多种输入对应多种输出——先把所有情况列出来再写代码,不容易漏)。

不适合 TDD 的场景也很明确:UI 样式调整(按钮颜色、间距这些,写测试没意义)、探索性原型(需求还没定,今天写的测试明天可能全废)、一次性脚本(跑一次就扔的数据迁移脚本,不值得写测试)。

小明不需要所有代码都用 TDD。但对于核心接口,"先写测试再写代码"这个习惯值得培养——它迫使你在动手之前先想清楚"这个东西应该怎么工作"。

而且 TDD 和 AI 编程配合得特别好。传统 TDD 的痛点是"写测试本身很费时间",但现在你可以用自然语言描述接口行为,让 AI 帮你生成测试代码。你的工作从"写测试代码"变成"描述接口应该怎么工作"——这其实就是设计。然后把测试文件交给 AI,让它实现代码。测试文件成了你和 AI 之间的"契约"——你定义行为,AI 实现行为,测试验证行为。这个工作流比"先写代码再补测试"高效得多,因为测试在前面就定义好了验收标准,AI 不需要猜你想要什么。

好奇的话展开看看:TDD 的完整流程
typescript
// 第一步:先写测试(RED——测试失败,因为接口还不存在)
describe('POST /api/movies/:id/reviews', () => {
  test('已登录用户提交短评', async () => {
    const res = await request(app)
      .post('/api/movies/42/reviews')
      .set('Authorization', `Bearer ${userToken}`)
      .send({ content: '很好看,特效震撼' })

    expect(res.status).toBe(201)
    expect(res.body.content).toBe('很好看,特效震撼')
    expect(res.body.movieId).toBe(42)
  })

  test('内容为空返回 400', async () => {
    const res = await request(app)
      .post('/api/movies/42/reviews')
      .set('Authorization', `Bearer ${userToken}`)
      .send({ content: '' })

    expect(res.status).toBe(400)
  })

  test('内容超过 500 字返回 400', async () => {
    const res = await request(app)
      .post('/api/movies/42/reviews')
      .set('Authorization', `Bearer ${userToken}`)
      .send({ content: 'a'.repeat(501) })

    expect(res.status).toBe(400)
  })

  test('未登录返回 401', async () => {
    const res = await request(app)
      .post('/api/movies/42/reviews')
      .send({ content: '很好看' })

    expect(res.status).toBe(401)
  })
})

// 第二步:写代码让测试通过(GREEN)
// 告诉 Claude Code:"这是我的测试文件,帮我实现对应的 API。"

// 第三步:重构(REFACTOR)
// 测试全绿后,优化代码结构,每次改完跑测试确认没破坏功能。

从"打地鼠"到"安全网"

回到小明的故事。一个月前,他改代码心惊胆战——每次提交都担心"会不会又坏了什么"。朋友的 bug 报告是他唯一的反馈渠道,而且总是迟到的。

现在不一样了。他的核心接口有 API 测试,关键流程有 E2E 测试。每次 git commit,Husky 自动跑测试——全绿,放心提交。代码推到 GitHub,Actions 再跑一遍——全绿,放心合并。某个测试红了?当场就知道是哪次改动引入的问题,不用等朋友来报 bug。

测试不是负担,是让你敢改代码的底气。没有测试的时候,你不敢重构——万一改坏了呢?有了测试,你可以大胆重构——改完跑一遍,绿了就没问题。测试覆盖越全,你改代码的信心越足。这种信心的转变是渐进的:第一次看到测试帮你拦住一个 bug("幸好有测试,不然这个 bug 就上线了"),你就会理解测试的价值不是理论上的,而是实实在在的。

这不是一夜之间建成的。小明的测试是逐步积累的:先给最容易出问题的接口写测试,再给核心流程加 E2E,然后配上 Git Hooks 和 CI。每次遇到一个 bug,修完之后顺手加一个测试——确保同样的 bug 不会再出现。这个策略叫"bug 驱动测试":不是提前预测所有可能的 bug,而是每次被咬一口就补一个防护。测试套件就这样慢慢长大,安全网越织越密。

不需要追求完美的测试覆盖率。80% 的风险可以用 20% 的测试覆盖。先把最关键的部分保护起来,剩下的慢慢补。重要的是开始——哪怕只有一个测试,也比零个强。从今天开始,下次你修完一个 bug,顺手写一个测试确保它不会再出现。这就是你的第一步。


本节核心要点

  • Git Hooks(Husky):提交前自动跑测试,测试失败阻止提交——第一道防线
  • GitHub Actions:推送后在干净环境里跑测试,发现"在我电脑上能跑"的问题——第二道防线
  • TDD 适合核心业务逻辑和稳定接口:先写测试定义行为,再写代码实现——不是教条,是工具
  • 测试是逐步积累的:先保护最关键的部分,每次修 bug 顺手加测试,安全网越织越密

下一步

第九章到这里就结束了。你已经有了测试的基本功和自动化工作流。接下来去 第十章:Localhost 与公网访问——把你的应用从本地搬到互联网上。