维护一个内部 Skill 仓库的体验比大多数人想象中要狼狈得多。你可能也有过类似的经历上次运行还很正常的技能这周突然在某个边界输入上报错新增了一个看似无关的小功能结果把老链路整个带崩上线前手工点了一遍没问题可用户换个说法就翻车。这些问题的根源其实很一致——Skill 的本质是一个承载不确定性的组件它接的输入是开放的输出又常常是非结构化的而大部分团队还在靠“写完随手点几下”来验证它。我在踩过几轮坑之后才彻底转向测试驱动开发TDD先把预期的行为固化成测试再让实现去满足测试。这个方法不需要什么高深工具但它能把“今天运气好没出问题”变成“每次改动都有据可查”。这篇内容会从为什么、怎么做、踩过哪些坑三个层面展开适合正在开发 Skill、Agent 插件或任何带 AI 能力的可复用模块的开发者参考。我会用一套可以照抄的工程化流程把 Skill 从“玄学”变成“可测试的确定性模块”。1. 为什么 Skill 是天然的 TDD 场景1.1 Skill 的“不稳定病根”在哪里先明确一下这里说的 Skill 是什么。它不是传统意义上函数或后端服务那样输入输出边界特别清晰的东西而是一个对外提供某种能力、内部往往依赖大模型或规则引擎的组合模块。举个例子一个“日程提取 Skill”输入是一句口语“明天上午十点开会下午记得买牛奶”输出是结构化的日程数组。听起来很简单但真正让它难以稳定的是三个特征输入不可穷尽、输出不完全可控、依赖项多且易变。输入不可穷尽意味着测试永远不可能覆盖所有说法。同样是表达“明天上午十点”用户可能说“明早十点”“明儿上午十点开会”“明天十点有个会”甚至夹杂着错别字和口语碎片。输出不完全可控则更棘手——大模型的输出是概率性的同一条输入跑两次结果可能有细微差别且 JSON 格式可能破损、字段名可能漂移、枚举值可能超出约定。更别说底层的模型接口会更新、Prompt 会被同事顺手改掉一个字、工具调用的返回结构会变动。这些因素叠加在一起就形成了“今天能用明天说不定哪里炸”的即视感。传统的手工验证对这种模块几乎无效。手工点的场景顶多十来个而 Skill 面对的输入空间是无限的更致命的是手工验证只能证明“当前这次跑通了”无法防止下一次改动引入回归。我在一个项目里就亲眼见过某同事为了修一个日期解析的小 bug把整个 Prompt 重写了一遍修好了 A 场景却让 B、C、D 三个场景全部静默失效——如果没有回归测试兜底这种问题会一直潜伏到用户反馈才暴露。1.2 TDD 不是测试技巧是设计工具很多人对 TDD 的第一印象是“先写测试再写代码让测试变绿”。但如果只看到这一层就会觉得它增加了工作量甚至是在做样子。实际用过之后我的感受是TDD 对 Skill 开发的作用被严重低估了它真正改变的是设计方式。TDD 的循环只有三步红让测试失败、绿让测试通过、重构在测试保护下优化代码。但“红”这一步逼着你先回答一个关键问题——这个 Skill 到底承诺了什么行为依赖项被 mock 掉之后输入 X 应该得到什么结构的输出边界输入应该怎么兜底异常情况下应该抛错还是返回默认值这些问题的答案会沉淀成一组测试用例而这些测试用例本质上就是 Skill 的行为契约。传统开发顺序是“先实现再补测试”测试往往变成了实现细节的复印件测的是“代码怎么写的”而不是“行为应该是什么”。TDD 反过来先定义行为再决定实现。尤其对 Skill 来说行为契约比实现重要得多——调用方只关心输入输出不关心你内部用的是哪个模型、写的什么 Prompt。用 TDD 开发 Skill你会被迫提前思考接口边界、错误处理、异常输入而不是只顾着让“happy path”跑通。还有一个容易被忽视的好处TDD 让重构变得安全。Skill 的 Prompt 调优是个高频操作有时候只是换个措辞输出质量就明显提升但同时可能破坏某些场景。有测试兜底你可以大胆地实验 Prompt 措辞、调整后处理逻辑、替换底层模型然后让测试告诉你有没有破坏约定。没有测试每次 Prompt 改动都像蒙着眼睛拆弹。1.3 Skill 场景下单元测试、集成测试怎么分工测试金字塔理论在这里同样适用但 Skill 场景的分工略有不同。单元测试针对的是 Skill 内部的纯逻辑部分包括输入预处理、输出解析、结构化校验、规则兜底等这些部分不依赖外部服务运行快、结果确定是 TDD 的主战场。集成测试则把真实的模型 API 或工具服务纳入进来用于验证 Prompt 实际效果、模型输出质量、整体链路是否顺畅。这两层测试的目的不一样不能互相替代。单元测试管的是“逻辑是否按照契约执行”集成测试管的是“模型是否按预期理解任务”。如果只写单元测试你可能把一层完美的解析逻辑架在一个根本产出不良输出的模型上如果只写集成测试速度慢、不稳定、且无法精确定位问题——一次失败到底是因为模型抽风还是解析 bug我的习惯是 80% 的测试放在单元层输入清洗函数、日期解析、JSON 归一化、schema 校验、异常分支全部用 mock 数据覆盖。剩下的 20% 是集成冒烟测试让 Skill 跑几条典型的真实请求做基础的质量断言。TDD 在这个框架下的角色非常清晰用单元测试把逻辑钉死用集成测试验证真实效果以稳定的代码逻辑为基座去拥抱模型的不确定性。2. 搭好测试基座Skill 测试骨架的一次到位2.1 测试框架与断言库选型测试框架的选择其实不用太纠结关键是与项目语言栈匹配。如果你的 Skill 是 TypeScript 生态比较顺手的组合是 vitest 加 supertestPython 生态则是 pytest 加 pytest-asyncio如果是 Java/KotlinJUnit 加 Mockito 也完全够用。框架本身不影响 TDD 的流程真正影响体验的是断言库的表达力和异步测试的支持度。Skill 场景有两个特性对测试框架提出了硬性要求。第一异步是常态因为 Skill 内部几乎一定会调用外部服务模型 API、工具服务测试框架必须原生支持异步用例否则半路会踩各种回调地狱。第二断言不仅要支持基础类型比较还要能对复杂结构做部分匹配因为模型输出的字段可能很多你只关心其中关键的几个。比如 TypeScript 生态我会用 vitest 的expect.objectContainingPython 用 pytest 加pytest-structlog或者直接配合 jsonschema 做校验——先把框架搭起来后续所有用例都会落到这层能力上。另外强烈建议在项目里接入覆盖率工具vitest --coverage或pytest --cov都可以。覆盖率不一定需要追求 100%但对 Skill 来说核心解析逻辑的覆盖率低于 80% 基本可以认为测试没写到位。原因后面会详细讲这里先埋个伏笔。2.2 目录与命名规范测试基建里最容易做错的事情是把测试文件和源文件混在一起但又不讲规矩。没有规则的测试目录短期看着省事长期一定会变成一锅粥文件名重复、用例不知道对应哪个模块、跑完测试不知道哪个挂了影响哪里。我目前的规范是这样的项目根目录下建独立的tests目录按模块做镜像结构。src/skills/todo-skill/parser.ts对应的测试放在tests/skills/todo-skill/parser.spec.ts解析逻辑的测试和 Skill 整体行为的测试分开建文件。命名上使用.spec.ts或.test.ts后缀测试框架默认就自动发现这些文件不需要额外配置。文件名只是基础真正重要是describe的层级要和被测结构一一对应。拿“日程提取 Skill”举例parser模块下会有describe(parser)作为顶层下面再按函数或场景拆成describe(extractDateTime)、describe(normalizeInput)。这样任何一个测试失败控制台输出的第一行就是“哪个模块的哪个行为出了问题”你可以直奔现场而不是从 30 个测试里玩排除法。这里还有一个实用的细节测试文件里不要写依赖执行顺序的用例。每个测试必须是独立的setUp 里给的数据当场构造不共享可变状态。Skill 测试最怕“上一个用例改了全局变量所以我挂了”这种幽灵场景一旦出现排查成本会指数级上升。2.3 让外部依赖“可控”Mock LLM 与工具调用Skill 测试最大的难点在于外部依赖不可控。模型 API 可能网络波动、可能限流、可能返回格式变化工具服务也存在同样的不确定性。如果不做处理你的测试就会变成“网络测试”而不是“逻辑测试”。所以测试基座里必须有一套成熟的 mock 策略。mock 的核心原则是mock 掉不稳定与慢的部分保留想验证的逻辑。对于 LLM 调用我通常会封装一个LLMClient接口真实环境注入模型 SDK测试环境注入一个返回固定响应的 fake 实现。比如要测试“日程提取 Skill 的解析函数”就 fake LLM 返回一段固定的 JSON 文本——包括正常的 JSON、带 markdown 代码块的 JSON、损坏的 JSON每个用例给一个 fixture解析逻辑在可控输入下逐行暴露。mock 工具调用同理。Skill 可能内部调用了日历服务、搜索接口或内部工具这些都要在外围 mock 掉。这里的技巧是不要 mock Skill 自身的函数而是 mock 它依赖的边界。用依赖注入的方式把外部服务变成可替换的接口而不是在测试里用vi.mock去偷换内部实现。这样测试的角度是“通过输入输出接口验证行为”而不是“验证内部函数的调用顺序”前者才是行为契约测试。mock 数据本身也很讲究。很多人随手捏一个很规整的 JSON 就当作 fixture这是大忌。真实世界的模型输出永远是脏的有多余的空白、带错误的转义、字段顺序不稳定、可能多出没约定过的属性。测试 fixture 应该从真实运行日志里截取再脱敏整理。这样测试才能真正模拟你害怕的场景而不是自欺欺人地测一个理想世界。3. 手写一个 Skill完整 TDD 循环直播3.1 需求拆解把一句话需求变成测试清单理论说太多没用直接来看实操。这里我用一个“待办提取 Skill”作为示例——输入一句口语描述输出一个结构化的待办清单。需求一句话就能说清“从用户描述中提取待办事项、时间信息和优先级。”但做 TDD 第一步不是写代码而是把这个需求拆成一份测试清单也就是行为契约。我会和需求方把边界问清楚遇到“明天十点开会”这种明确时间怎么处理遇到“记得买牛奶”这种没有时间只有事项的呢优先级怎么定用户不说就默认普通吗输入为空或纯空白字符串是抛异常还是返回空数组日期格式五花八门是统一转成 ISO 还是保留原文这些问题看着琐碎但每个答案都会变成一个测试用例。最终这份清单大概长这样输入正常文本输出包含所有待办事项带明确时间的输入解析出正确的时间字段输入无时间信息时时间字段为空但待办不丢输入为空字符串返回空数组而不是抛异常输入纯空格与空字符串行为一致多个待办事项同时存在顺序与输入一致以“赶紧/务必/尽快”等语气词识别为高优先级模型的返回是带 markdown 代码块的 JSON仍能正确解析模型返回损坏 JSON 时返回降级处理结果而不是崩溃这就是 TDD 的红阶段设计。测试清单不需要一次写完你完全可以在第一轮先写三五个核心用例之后的循环里慢慢补充边界。但有一个原则每个测试必须表达一种“可观察的行为”而不是“内部实现的细节”。比如“调用 schema 校验”“解析 JSON 时 catch 了异常”这种用例就属于实现细节写了反而增加维护负担。3.2 第一轮红先写失败测试选一个核心行为开始例如“输入正常文本输出包含所有待办事项”。我先把 Skill 的接口定下来extractTodos(input: string): TodoItem[]。TodoItem包含title、time、priority三个字段。然后写测试用 vitest 写法大概是这样的import { describe, it, expect } from vitest; import { createFakeLLM } from ../test-utils/fake-llm; import { extractTodos } from ../../src/skills/todo-skill/todo-extractor; describe(extractTodos, () { it(从正常输入中提取所有待办事项, async () { // 构造 fake LLM 返回 const llm createFakeLLM({ response: JSON.stringify([ { title: 开会, time: 2025-06-20T10:00:00, priority: normal }, { title: 买牛奶, time: , priority: normal }, ]), }); const todos await extractTodos(明天上午十点开会下午记得买牛奶, { llm }); expect(todos).toHaveLength(2); expect(todos[0].title).toBe(开会); expect(todos[1].title).toBe(买牛奶); }); it(输入为空字符串时返回空数组, async () { const llm createFakeLLM({ response: [] }); await expect(extractTodos( , { llm })).resolves.toEqual([]); }); });注意测试里我把 LLM 客户端作为参数注入进去。这是 TDD 里很关键的设计选择extractTodos(input, deps)这样写测试就不需要任何全局 mock也能让依赖关系在函数签名里暴露得明明白白。此时extractTodos还没有实现两个测试必然失败——这就是我们想要的“红”。看到红灯不要慌这正是 TDD 起效的证明测试真的在验证行为而不是空转。3.3 第二轮绿写最小实现通过测试红的目标是让我们看到“行为缺失”的信号绿的目标则是用最小实现让测试通过。所谓最小实现就是代码宗旨不是优秀、不是完整而是“刚好满足当前测试的描述”。这段过程很多人容易走偏——会忍不住一次性把完整功能写全觉得顺带实现让我省事。但这么做恰恰破坏了 TDD 的节奏因为一旦实现超越了测试的约束你写的那些“额外功能”就没有测试保护后面一重构就碎。对于上面两个测试最小实现可以是import { LLMClient } from ../types; export interface TodoItem { title: string; time: string; priority: high | normal; } export async function extractTodos( input: string, deps: { llm: LLMClient } ): PromiseTodoItem[] { const trimmed input.trim(); if (!trimmed) return []; const response await deps.llm.complete( 提取以下文本中的待办事项返回 JSON 数组。\n\n${trimmed} ); const todos JSON.parse(response); return todos; }只看这段代码甚至没有错误处理没有日期规范化但两个测试都过了。这个阶段代码是“丑”的没关系TDD 的红绿循环里重构是下一步才做的事。重要的是我建立了一个安全的垫脚石后续再添加测试、再改实现都有这两个测试盯着不会在不知不觉中退步。这里顺便解释一个会被新手误会的点TDD 不是说代码永远不完善而是说“每一次功能扩展都是由测试驱动的”。每次你想加错误处理逻辑或降级策略先写一个会失败的测试用例再去实现。这样代码的每条行为都是被测试锚定的而不是“我记得这里应该处理一下”的裸奔。3.4 第三轮重构稳定性加固在第一轮红绿通过后测试清单还剩下很多边界用例。我不会一口气全写而是挑一种最危险的情况来开始模型的返回带 markdown 代码块。这是实际中最常见的坑——模型喜欢把 JSON 包在json里直接JSON.parse必挂。先写测试it(解析被 markdown 代码块包裹的 JSON, async () { const llm createFakeLLM({ response: json\n[{title: 开会, time: , priority: normal}]\n, }); const todos await extractTodos(明天十点开会, { llm }); expect(todos).toHaveLength(1); expect(todos[0].title).toBe(开会); });红灯因为JSON.parse处理不了 markdown 代码块。然后实现一个最小修复先提取 JSON 部分再解析。此时我会顺势处理另一个高风险场景损坏 JSON 时的降级。再补一个测试it(模型返回损坏 JSON 时降级为基于标题的简单解析, async () { const llm createFakeLLM({ response: { title: 开会 } }); const todos await extractTodos(明天十点开会, { llm }); expect(todos).toHaveLength(1); expect(todos[0].title).toBe(明天十点开会); });这个用例的意图很明确Skill 不能因为模型输出异常就让整个链路崩溃至少要退化成“把原文当作待办标题”的保底方案。实现可以用正则抓 JSON 片段拿到干净字符串就尝试解析失败就进入兜底逻辑。这种“红绿循环”每轮只加一个行为代码一步步从直白变得健壮。积累几个用例之后你会发现 Skill 的稳定性不是靠一次设计出来的而是靠测试清单逐步钉出来的。每修完一个边界测试集合就更密行为契约就更完整。这也是为什么我说 TDD 对 Skill 开发不是可选项而是保证“稳定性”这个目标最省力的路径。4. 稳定性不只是“能跑”还要“扛得住”4.1 边界值与异常输入的回归网很多人理解的测试是“验证功能正常”但 Skill 的稳定性建设里边界测试的价值远高于普通功能测试。一个“把明天上午十点变成 ISO 时间”的解析函数最容易挂掉的不是标准情况而是“明天”“后天”“下周一”“周五晚上”“3月5号下午三点”这种五花八门的自然语言表达。边界测试的设计思路是绕着非法和异常来布网。空字符串、纯空白、超长输入、emoji 混杂、全是标点符号、只有日期没有事项、只有事项没有日期、长文本里有多个待办事项但互相嵌套——这些用例不必一次写完但每遇到一个真实故障就把它固化为一个回归测试。这是稳定性建设的最低成本做法线上用户帮你踩过的坑必须变成自动测试拦住它再犯。我在实际项目里有个铁律线上 bug 修复必须附带回溯测试。不是说修完代码就完事而是先写一个能复现该 bug 的测试确认它红灯再修代码让测试变绿。这等于给每个历史故障立了一块墓碑提醒后续改动不要在同一个位置跌倒。这种做法积累两三个月后Skill 的回归网会厚到让你对重构有底气。4.2 输出结构校验让 LLM 返回不可信成为历史LLM 输出的结构不稳定是 Skill 稳定性的头号敌人。你让它返回固定的 JSON它可能给你加字段、漏字段、字段名拼错甚至给你返回一段自然语言。单元测试能覆盖 mock 数据下的情况但真实环境里模型比你想象的更不可控。所以在 Skill 内部必须有一个结构校验层让“模型乱输出”不至于直接污染下游。我最常用的做法是引入 JSON Schema 校验。定义好TodoItem的 schema模型输出经过解析后先用 schema 校验合法则放行不合法则尝试修复或降级。这一层逻辑也可以遵循 TDD——先写一个“模型输出缺少必填字段”的测试再实现校验与降级策略。import { z } from zod; const TodoSchema z.object({ title: z.string().min(1), time: z.string().optional().default(), priority: z.enum([high, normal]).default(normal), }); export function parseTodos(jsonText: string): TodoItem[] { const raw JSON.parse(jsonText); if (!Array.isArray(raw)) return []; return raw .map((item) { const parsed TodoSchema.safeParse(item); return parsed.success ? parsed.data : null; }) .filter((x): x is TodoItem x ! null); }这段逻辑把“模型输出不可信”的问题隔离在了一个小函数里。后续模型再怎么乱来最多走到这个函数被过滤掉不会让脏数据流入业务逻辑。而且这个函数是纯函数——不依赖外部服务输入输出确定可以安心地堆测试覆盖。我建议所有 Skill 都把“脏输入 → 结构化干净数据”这个环节做成纯函数这是整个模块最值得投资测试的地方。4.3 覆盖率与测试金字塔覆盖率是测试建设的仪表盘但怎么看待它需要一点辩证。对 Skill 而言盲目追求行覆盖没有意义——你覆盖了一堆内部细节却漏掉了最危险的行为边界。我更推荐以“分支覆盖”为参考关注每个函数里if/else、switch、||等分支是否都走到了。核心解析逻辑分支覆盖率达到 80% 以上基本上可以放心重构。如何判断哪些是核心逻辑看一个标准出问题后影响面多大。输入清洗、输出解析、字段映射、时间规范化、schema 校验——这些是 Skill 的“骨架”必须重兵把守。而 Prompt 拼接、工具调用顺序这些相对稳定或难以断言的可以适度放过。把测试资源投在回报最高的地方而不是雨露均沾地追求覆盖率数字。测试金字塔在 Skill 领域的分工也很明显。底层的纯函数测试最多最细运行毫秒级适合频繁执行。中间层的模块测试把“输入文本 → 调用 mock 依赖 → 输出结构化结果”整条链路跑通。顶层的集成测试数量少、代价高只在发布前跑。这个金字塔的比例大概控制在 8:2:1 比较舒服能保证反馈速度和稳定性的平衡。5. 与 CI/CD 结合让测试成为 Skill 发布的守门员5.1 CI 里跑哪些测试测试写好了如果只在本地手动跑价值会大打折扣。真正的稳定性红利来自把测试接入流水线每次提交代码自动跑一遍全量用例把回归消灭在与仓库交互的那一刻而不是等上线后被用户发现。本地开发阶段跑的是快速测试组——纯函数和 mock 依赖的模块测试因为不访问真实服务几秒钟就出结果适合在编辑器里频繁执行。CI 阶段则建议分两层合并请求MR触发时跑全量单元测试与静态检查发布阶段再跑集成测试因为集成测试依赖真实环境且耗时较长没必要每次提交都折腾一遍。持久化测试结果报告也值得做。CI 上发布的测试趋势图可以直观看到用例数量、覆盖率、失败曲线的变化。我曾在一个项目里发现单元测试数量一直在涨但覆盖率跌了——说明新增测试都挤在了同一类用例上其他模块依然裸奔。如果没有趋势图这种隐性恶化很难被察觉。5.2 测试即文档从测试用例反推 Skill 行为契约测试还有一层很少被大家重视的价值它是最靠近代码的行为文档。Skill 这种模块文档通常滞后于代码Prompt 一改需求文档就过时了。但测试是即时更新的——每次行为变化对应的测试必然跟着变。因此我建议给 Skill 仓库建一份BEHAVIOR.md但内容不是空对空的描述而是直接从测试清单导出的行为列表。写清楚当前版本支持的输入类型、边界情况、降级策略、错误处理方式。这份文档的维护成本极低因为每一次 TDD 循环新增的测试用例本身就是在扩充行为列表。新成员接岗时看测试比看文档有效得多——测试展示了具体的示例输入与期望输出相当于嵌在代码里的活文档。另一个实践是给测试用例分标签或用命名前缀做分类。比如[core]表示核心契约[edge]表示边界行为[regression]表示历史故障回归。这样 CI 失败时一眼就能判断严重等级[core]挂了说明行为契约被破坏了必须立即处理[edge]挂了可能是边界策略需要调整。分类还能辅助“哪些用例能删除”的决策——[regression]用例原则上永不删除因为它代表的是线上真实故障的防线。6. 常见问题与排查技巧实录6.1 问题速查表真刀真枪跑起来你会遇到不少看着简单、实际坑人的问题。我整理了一张速查表都是 Skill 测试落地过程中的高发问题每条后面附了排查思路和解决方案。现象可能原因解决方案测试时而通过时而失败依赖了真实 LLM API网络或模型波动影响使用 fake LLM把外部依赖隔离在测试之外JSON.parse抛错模型输出带 markdown 代码块或多余文本实现 JSON 提取与规范化函数并测试覆盖用例执行顺序影响结果测试共享了全局可变状态每个用例独立构造数据不要在模块级缓存结果覆盖率数字一直上不去测试集中在 happy path分支没覆盖引入 mutation testing 或检查分支覆盖补边界用例一个用例失败导致一片用例失败beforeEach 里状态污染检查公共 fixture 是否被修改建议用工厂函数替代共享对象Prompt 改动后全量测试惊喜不断Prompt 输出结构与预期不一致在 Prompt 输出后加 schema 校验层保证下游逻辑不受影响测试跑得越来越慢用例里堆积了大量重量级依赖初始化把纯逻辑抽离成纯函数缩小 mock 范围用轻量 fixture新功能写了但忘了写测试TDD 节奏被打破直接进入实现回到红绿循环先补失败用例再实现或至少补回归用例这张表不一定覆盖你遇到的所有问题但它是排查方向的起点。Skill 测试的大多数诡异故障追根溯源都绕不开“外部依赖不可控”和“测试间状态隔离不彻底”这两个基本盘。6.2 踩坑心得过度 Mock、铁铲断言、测试数据污染最后分享几个我一个人扛过之后才真正理解的教训。第一个教训是过度 mock。有一段时间我给 Skill 测试写了很多 mock把内部依赖全都替换成理想返回值测试全绿但一上线就出问题。原因是 mock 得越深测试距离真实行为越远它验证的是“我 mock 的接口格式对不对”而不是“Skill 在实际环境中能不能完成任务”。后来我调整策略测试里 mock 只到边界LLM 接口、外部工具但内部完整链路——输入清洗、调用逻辑、解析、校验、降级——全部走真实代码。第二个教训是铁铲断言。这个词是我自创的指断言太贴近实现细节把代码重构锁死。比如测试里断言了“内部调用了某函数两次”“某个中间变量应该等于什么”一旦重构实现方式测试就碎一地但行为其实完全没变。正确的做法是断言输入输出的外部可观察行为以用户视角验证结果而不是以开发视角检查内部动作。第三个教训是测试数据污染。有一回我在 fixture 里硬编码了一个真实用户导出的排期数据里面带了一堆隐私字段推上仓库后被安全评审拦截。现在已经学乖了一切测试数据都走“合成数据”路线用可复现的随机生成器关键的边界场景手写造数明确禁止从生产环境复制数据。测试是代码库的一部分它的安全级别应该和生产代码一样高。还有一个容易被忽略的细节测试命名。很多人写it(test1)、it(should work)这种毫无信息量的命名失败的时候你只看得到“should work failed”完全想不起来这是哪个行为。好的命名应该是一句话的行为描述it(输入为空字符串时返回空数组而不是抛异常)。这句话不仅是给机器跑的更是给未来的维护者读的行为契约摘要。Skill 的稳定性建设不是一锤子买卖它更像是一个持续加固的过程。我从一开始的手工验证到后来被线上问题追着跑再到老老实实 TDD最大的变化不是测试数量变多了而是开发心态变了每次改动前先想想“我要改变什么行为”让测试来回答“我有没有改坏其他行为”。如果你也在维护 Skill 类模块不妨从下一个需求开始试着先写两条失败测试再动手实现。这个习惯用上两三个迭代之后你对“稳定”的定义会发生根本性的改变。
阅读完成 · 觉得有帮助?