在前两篇文章中,我们分别讨论了:
- 第一部分:确保需求本身的质量(文档化、正确性、完整性)
- 第二部分:确保需求能被正确理解(无歧义性、一致性)
现在,需求已经"合格"且"清晰"了,但还有一个关键问题:需求能否被正确实现和验证? 本文重点讨论可测试性、可追溯性和可行性,这三个特质确保需求不仅能被理解,还能被实际执行、验证和维护。
可测试性
我们必须有一些想法来测试需求是否得到满足。如果可以通过实际和客观的方式来确定实现的解决方案是否符合需求,则该需求是可测试的。可测试性对于人类生成的代码和人工智能生成的代码都至关重要。我们的信心必须主要来自验证代码行为。针对清晰、可测试的需求进行严格测试是确保代码可靠且符合目的的主要机制。可测试的需求为验证提供了蓝图。
可测试性要求小粒度、可观察性和可控性。这三个特性就像测试的三根支柱,缺一不可。
小粒度
这里的小粒度需求意味着它产生了一个小的被测代码单元。这就是可分解性、简单性和模块化变得重要的地方。具有单一职责的小型、定义良好且简单的代码单元比大型、整体化和复杂的组件更容易理解、全面测试和推理。如果人工智能生成了一个庞大且纠缠的函数,即使它在"快乐路径"上"工作",验证其所有内部逻辑和边缘情况也极其困难。你无法确定其中可能潜伏着什么意外行为。为了实现小粒度,将大型需求分解为更小、更易于管理的子需求。每个子需求最好描述一个单一、连贯的功能及其自身的可测试结果。就像拆积木,小块比大块更容易测试。
可观察性
可观察性是指根据输入确定组件的内部状态及其输出的难易程度。这在测试执行之前、期间和之后都是如此。本质上,你可以"看到"软件在做什么以及它的结果是什么吗?为了测试,我们需要能够观察行为或状态。如果一个动作的效果完全是内部的且不可见,那么测试就很难进行。就像黑盒子,你看不到里面发生了什么,就很难测试。
为了实现可观察性,我们需要清晰且全面的日志记录,通过 getter 或状态端点公开相关状态。我们需要返回详细且结构化的错误消息,实现事件发布,或者有效地使用调试器。这样我们就可以验证中间步骤,理解执行流程,并诊断测试失败的原因。
具体要求包括:
- 描述外部行为:关注系统可以被看到的行为,而不是它内部是如何实现的(除非内部的"如何"影响了需要约束的非功能性需求,如性能)。
-
指定输出:详细说明任何输出的格式、内容和目的地(UI 显示、API 响应、文件生成、数据库条目、日志消息)。
- 示例:在成功注册后,系统必须返回一个带有 JSON 正文的 HTTP 201 响应,其中包含
user_id和email。
- 示例:在成功注册后,系统必须返回一个带有 JSON 正文的 HTTP 201 响应,其中包含
-
定义状态变化:如果状态变化是一个重要的结果,指定如何观察该状态。
- 示例:在订单提交后,订单状态必须是"PENDING_PAYMENT",并且可以通过
/orders/{orderId}/status端点检索到该状态。
- 示例:在订单提交后,订单状态必须是"PENDING_PAYMENT",并且可以通过
要求记录关键事件:以 INFO 级别记录关键状态变化和决策点。系统必须在成功登录时记录一个带有
event_type='USER_LOGIN_SUCCESS'和user_id的审计事件。
可控性
可控性是指我们"引导"组件进入特定状态或条件的难易程度。我们能够多容易地为组件提供必要的输入(包括依赖项的状态),以执行测试并将其与测试无关的外部因素隔离?我们可以通过依赖注入(DI)、设计清晰的 API 和接口、为依赖项使用模拟对象或桩以及提供配置选项等技术来实现这一点。这使我们能够轻松地设置特定场景,独立地测试单个代码路径,并创建确定性的测试。就像做实验,你需要能控制变量,才能得出准确的结论。
可控性差导致的问题
如果可控性不好,会遇到以下问题:
硬编码依赖项
它们可能会迫使你将单元测试与真实的依赖项一起进行。这会使单元测试变成缓慢且可能不可靠的集成测试。你无法轻松地模拟依赖项的错误条件。就像测试一个函数,但函数内部硬编码了数据库连接,你就必须真的连数据库才能测试。
依赖全局状态
如果一个组件读取或写入全局变量或单例,测试很难隔离。一个测试可能会改变全局状态,导致后续测试失败或行为不可预测。在测试之间重置全局状态可能很复杂。全局状态就像公共厕所,一个人用了,下一个人用的时候可能就不干净了。
缺乏清晰的输入机制
如果一个组件的行为是由复杂的内部状态变化触发的,或者依赖于不透明的数据源,而不是清晰的输入参数,那么很难将其置于特定测试所需的特定状态。就像黑盒子,你不知道怎么控制它,就很难测试。
后果
- 测试缓慢:需要设置数据库、调用真实 API 或等待真实超时的测试运行缓慢,这会阻碍频繁执行。
- 测试不稳定:依赖外部系统或共享状态的测试可能会因测试代码之外的因素(例如,网络问题、API 速率限制)而间歇性失败。
- 难以编写和维护:复杂的设置和非确定性行为导致测试难以编写、理解和调试失败。测试的"准备"阶段成为一个巨大的努力。
可追溯性
软件需求的可追溯性意味着能够正向和反向跟踪需求的生命周期。你应该能够将特定需求与实现和验证它的设计元素、代码模块和测试用例联系起来。反之,查看代码片段或测试用例时,你应该能够追溯它所满足的需求。可追溯性告诉我们代码存在的原因以及它应该实现的业务规则或功能。如果没有这种联系,代码可能会迅速变成开发者不敢触碰的"魔法"。就像给代码贴上标签,你知道它是为什么而写的。
1. 调试和根本原因分析
当人工智能生成的代码出现错误或意外行为时,追溯到源需求通常是第一步。需求有缺陷吗?人工智能误解了一个正确的需求吗?可追溯性指导调试过程。有了可追溯性,你就能快速找到问题的根源。
2. 维护和变更影响分析
需求不可避免地会变化。如果需求 REQ-123 被更新,可追溯性允许你快速识别特定的代码部分(可能是人工智能生成的)。与 REQ-123 相关的测试需要审查、修改或重新生成。没有可追溯性,找到所有受影响的代码部分将是一个耗时且容易出错的手动搜索。就像改一个需求,你知道要改哪些代码和测试,不会漏掉。
3. 验证和覆盖
可追溯性有助于验证我们的需求是否有代码和测试。你可以检查是否有需求被遗漏,或者是否有生成的代码无法追溯到有效需求。这样你就知道哪些需求实现了,哪些还没实现。
可行性
如果需求可以在项目的给定约束内实际实现,则该需求是"可行的"。这些约束通常包括可用时间、预算、人员技能、现有技术栈、架构模式、安全策略、行业法规、性能目标和部署环境。
明确约束的必要性
为了确保人工智能助手生成可行的代码,需求必须明确说明相关约束。这些约束作为护栏,引导人工智能朝着不仅是技术上可能,而且对于你的特定项目背景也是实用和适当的解决方案发展。也许你的公司标准化了使用 FastAPI 框架用于 Python 微服务。也许某些服务直接访问数据库被安全策略禁止。也许你的部署目标是一个低内存容器环境,或者人工智能建议的特定外部(付费)API 超出了项目预算。就像给 AI 画个框,告诉它只能在这个框里发挥,这样生成的代码才符合你的实际情况。
总结
当为人工智能生成的代码编写需求时,基本原则保持不变,但重点转向:
- 极度明确性:精心涵盖边缘情况、错误和非功能性需求。不要指望 AI 能自己推断,要把能想到的都写清楚。
- 无歧义性和精确性:使用清晰、机器可解释的语言。避免模糊表达,用具体的数据和规则。
- 约束定义:通过指定架构、技术栈、模式和非功能性需求来引导人工智能。告诉 AI 你的边界在哪里。
- 可测试性:定义清晰、可衡量的验收标准。小粒度、可观察性和可控性很重要。让需求可测试,代码才能可靠。
- 结构化输入:以最佳方式为人工智能呈现需求。用模板、格式化的方式组织需求,AI 更容易理解。
本质上,人工智能代码生成的需求意味着更加深思熟虑、详细和指令性。这是关于为人工智能提供一个高保真度的蓝图,以减少猜测。一个最大化生成正确、安全、高效和可维护代码的概率的蓝图。与项目目标和技术标准一致的代码。这涉及增强完整性、无歧义性和可测试性等特质的重要性。这也涉及演变对"可理解性"的解释,以适应人工智能"开发人员"。
简单来说,以前的需求文档是给人看的,现在还要给 AI 看。AI 不像人那样有常识和经验,所以需求必须写得更详细、更明确、更结构化。 虽然写需求的工作量增加了,但换来的是更可靠的代码生成,这个投入是值得的。
目前看来,精心编写软件需求也可以减少人工智能生成代码中的幻觉现象。然而,仅通过需求本身并不能完全消除幻觉。输入提示(包括需求)的质量和结构显著影响人工智能产生幻觉的可能性。幻觉还源于模型限制、训练数据的痕迹以及提示上下文的边界。这些因素超出了本文的范围。
最后,记住一个原则:好的需求文档,不仅能让 AI 生成好代码,也能让人更好地理解系统。 这是一举两得的事情,值得花时间做好。