| 编辑推荐: |
本文介绍了如何利用AI加速C/C++微控制器的固件单元测试:通过将重复性测试需求自动化并生成初始测试代码,把原本约3小时的手工测试开发工作压缩到1小时左右。文章涵盖目标板外/目标板上测试的权衡、提示词的编写与迭代、AI输出的评审方法、人机协同(Human-in-the-Loop)实施手册以及ROI分析,希望对您的学习有所帮助。
本文来自于Fresh Consulting,由火龙果软件Linda编辑、推荐。
|
|
对微控制器上的C/C++代码进行健壮的固件单元测试,其成本是嵌入式开发中最顽固的瓶颈之一——它常常吞噬大量时间,导致关键的质量保障工作被推迟甚至被省略。传统的测试框架和自动化工具虽然可以处理环境搭建和样板代码部分,但要实现真正全面的单元测试集,仍然需要耗费大量时间来手工设计、编写和调试测试代码。这种高门槛导致缺陷和故障在流程后期才被发现,推高了成本并削弱了产品的稳定性。
但是,如果开发和实现测试用例所需的时间能够从3小时的手工工作压缩到1小时呢?
本文内容包括:
- 一个经过验证的真实工作流程:在一个中等规模的嵌入式项目中,利用人工智能(AI)克服了固件单元测试的瓶颈。
- 深入探讨如何利用现代AI API构建定制方案,分析C源代码并自动生成C++单元测试,从根本上加速工作流程。
- 测试嵌入式软件的独特挑战——包括目标板上(on-target)与目标板外(off-target)测试之间的权衡;随后界定AI在此场景下的具体能力与关键局限。
- 提供一套可落地的人机协同(Human-in-the-Loop)实施手册,拆解“3小时缩短到1小时”的全过程,并给出投资回报(ROI)的判定指南,帮助团队判断这种AI加速方法何时值得采用。
1.嵌入式软件项目中固件单元测试的成本
可用于实现单元测试的工具和框架非常多,多到很难为某个项目或应用选出最优方案。市面上有大量扩展和其他工具可以生成并搭出单个测试的骨架,但其中大多数都无法自动生成测试内容本身。对于一些较简单或定义明确的测试准则(如fuzz模糊测试或MC/DC测试),可以自动生成测试代码;但更普遍的情况是,实现一套良好的单元测试需要程序员花费大量时间手工设计和编写测试代码。
而当要测试的是嵌入式系统的代码时,难度和耗时还会进一步增加。可以脱离目标系统进行测试(off-target),这样能使用更丰富的测试工具,但代价是无法检查时序以及其他硬件特有属性和行为的影响;也可以在目标硬件上运行测试(on-target),但这会带来额外的复杂性,并且需要花更多时间搭建必要的测试工具链,才能在硬件上完成编译、运行和验证。在目标板上测试还会让自动化测试和持续集成的实施难度与成本大幅上升。
其结果是,嵌入式项目开展固件单元测试的门槛相当高,往往只有较大的项目或面向安全应用的固件才会做测试。因此,缺陷和故障被发现得更晚,造成进度延误、成本超支,产品也不够健壮。
2.引入AI后会发生什么改变
将AI引入其中,从根本上改变了固件单元测试的成本收益方程,为大幅降低测试设计与实现所需的时间和成本提供了一条可行的路径。本节将探讨:AI如何将重复性的需求自动化并生成初始测试代码;为什么保持较小的任务范围是有效发挥AI能力的关键;以及在AI生成代码与人工评审之间如何取得必要的平衡。
2.1 用AI自动化重复性的测试需求
单元测试的需求往往相当重复。例如,针对输入的边界情况(corner case)应当编写测试,而这类要求可以套用到每一个被测函数上。
这组重复性的输入要求很容易实现自动化,并与待测源代码一起喂给AI。这种提示词(prompt)的自动化,使我们可以快速、低成本地生成测试桩,以及测试代码的第一稿。
2.2 保持小范围以获得最佳效果
关键在于把上下文和任务范围控制得足够小,让AI能够完成设计测试所需的复杂分析并正确地实现它们。范围太大时,当前的AI系统会犯太多错误,需要人工反复修正,生产率提升会被削弱甚至完全抵消。
被测源代码越大、越复杂,AI犯的错就越多。10行以内的简单函数,AI生成的代码通常不需要任何修正;约100行以内、圈复杂度适中(约20以内)的代码,当前的AI仍能有效处理;而更大、更复杂的代码,AI就会开始大量出错,需要开发者投入大量精力去评审和修复。
2.3 在AI效率与人工监督之间取得平衡
评审和调试AI生成的测试代码,远比让开发者从头编写再调试要快得多。尽管有这种效率提升,当前AI系统的准确性还不足以正确生成所需的全部测试代码;因此,仍然需要开发者进行一定的评审和修改,才能得到有用且可靠的测试。
总结一下:
- 通过将重复性需求自动化并生成初始测试代码,AI为大幅降低单元测试设计与实现的时间和成本提供了一条可行的路径。
- 有效利用这项技术的关键在于保持小而可控的任务范围,因为随着源代码变得更大、更复杂,当前AI的表现会迅速下降。
- 虽然生成的测试代码仍需人工评审和打磨,但与完全手工开发和执行测试相比,效率的提升使AI成为提高测试过程整体速度和可靠性的宝贵工具。
3.AI生成单元测试的人机协同实施手册
实施固件单元测试并利用AI进行代码生成的总体工作流程如下:
- 定义测试准则
- 针对这些准则,决定采用目标板上测试、目标板外测试还是两者相结合的方式
- 选择并搭建测试框架
- 编写提示词(prompt),让AI生成满足准则的单元测试
- 对提示词进行测试与打磨
- 生成单元测试
- 人工评审并调整测试代码
- 使用这些测试
与更传统的工作流程相比,这种AI增强流程的主要差异在于:
- 对测试准则进行调整,使其便于被AI处理
- 生成AI提示词并对其进行迭代
- 评审AI输出的正确性并进行修正
下面各节将逐一展开说明。
3.1 定制测试准则
要把测试准则喂给AI,只需将其整理成纯文本格式即可。有些AI还能处理PDF等其他文档,因此有时连这一步都可以省略。
但AI并不具备任何组织内部的知识积累,因此测试准则必须详细、明确地写出来,包括期望的格式,才能得到一致且符合最佳实践的结果。一般来说,要求写得越详细、越具体,从AI获得的结果就越好。
如果需要为每个被测单元传入特定的要求,借助现有的AI API也相对容易实现;但这些要求必须以AI能理解的格式记录,并与适用的单元关联起来,才能把正确的单元需求传给AI。
3.2 清晰、准确的AI提示词至关重要
当前AI系统的表现很大程度上取决于输入数据和指令的质量(“垃圾进,垃圾出”)。模糊或过于简单的提示词往往会产生不一致的低质量输出。因此,要获得最佳结果,向AI提供详细而具体的指令至关重要。这个过程类似于编写一份完整细致的作业指导书:比如指导别人更换轮胎,仅说“拆下坏胎、装上备胎”是不够的,必须有逐步、具体的操作过程才能顺利完成。同样,AI也需要明确、结构化的指令,才能生成期望的高质量结果。
迭代式的提示词甚至比一次性给出详尽提示词的效果更好。例如,把“编写执行修正条件/判定覆盖(MC/DC)测试”的提示词拆成四个步骤,往往能得到质量更高的AI测试代码:
- 找出<被测函数>中的所有判定点
- 基于你找出的判定点,编写一组执行MC/DC测试的单元测试
- 评估你编写的MC/DC测试覆盖了哪些判定点
- 是否还有判定点未被充分测试?如有,补充测试来覆盖它们。
除了清晰的指令之外,AI也不知道自己的角色定位——让它扮演一个试图看懂C源代码的画家,和让它扮演资深软件开发专家,期望得到的回答会有天壤之别。因此,明确告诉AI它的角色,有助于确保AI恰当地响应你的指令,从而更稳定地输出所需类型和质量的结果。
3.3 评审AI输出
现代AI远非完美。即使是为难度中低等的源代码生成测试,AI仍会犯错,或者更常见地做出错误的假设。AI生成的代码必须由人工评审,以确保其适用于目标用途。不过,评审并修复一份“已完成95%”的代码,远比从零开始编写测试代码快得多。生成的测试固然需要评审,但借助AI完成测试初稿仍然能节省大量时间。
4.从3小时到1小时的周转过程拆解
为了更具体地展示上述步骤,下面分析一种方法:使用Google Test和Google Mock,在Linux环境(脱离目标板)下为ARM微控制器运行测试。这套方案最近在一个中等规模的嵌入式项目中得到了成功应用。
4.1 被测源代码示例
下面是瑞萨(Renesas)某款ARM微控制器FSP驱动库的节选。在本示例中,目标是让AI为crc_seed_value_update函数编写一些单元测试用例。该函数用于更新硬件CRC模块所使用的CRC种子值。
测试这段代码的一个挑战在于:单元测试不会运行在目标硬件上,因此测试系统中并不存在那些寄存器,直接写寄存器会导致段错误(segmentation fault)。针对硬件寄存器的一种通用解决办法,是利用构建环境把寄存器的固定地址替换为RAM中分配的内存。在本例中,R_CRC将是一个指向结构体的指针。
4.2 需求规格说明
下一步是制定一组需求,供AI在为源代码创建单元测试时遵循。下面是一个需求规格说明示例,适用于简单的单阶段AI提示词。
请注意,尽管这是一个相对简单的示例,需求却非常详细和具体。对期望输出描述得越清晰,AI就越能更好地满足这些需求。如果再把AI提示词拆分为多轮“请求—应答”交互,还可以获得更好的结果。
单元测试将使用C++编写,并使用Google Test和Google Mock库。crc_seed_value_update是被测函数。
crc_seed_value_update必须拥有一组单元测试(TEST_F实现),要求:
1) 覆盖每一条代码路径并检查操作是否正确。期望达到100%的语句覆盖和分支覆盖。
2) 输入无效或错误的值,并验证crc_seed_value_update能够检测并妥善处理这些错误输入。
3) 使用提供的Google Mock接口,验证在包含crc_seed_value_update的源文件之外发生的所有函数调用均被正确执行。
4) 使用提供的Google Mock接口,让函数调用返回错误值或错误码。测试应验证crc_seed_value_update能够检测
并处理这些错误返回值。
5) 任何数学运算都必须测试其正确性。
6) 在可能的情况下,检查crc_seed_value_update内部的指针赋值是否正确且有效。
在代码的开头,创建一个注释块,把每个TEST_F映射回上述需求列表。对每条需求,列出用于满足该需求的所有TEST_F。
同时,在TEST_F列表中,简要说明每个TEST_F是如何满足对应需求的。例如:
<用于满足需求1的测试:
DIO0_IRQHandler_IgnoresInterruptWhenIgnoreFlagSet - 检查IgnoreFlag置位时的代码路径。
DIO0_IRQHandler_ButtonPressedTogglesStatusFromZero - 检查IgnoreFlag未置位时的代码路径。>
每个单元测试必须:
1) 只测试单一条件。
2) 使用Google Test的TEST_F宏来实现测试。
3) 命名采用如下格式:crc_seed_value_update_[被测操作],其中[被测操作]用于描述该TEST_F正在测试的内容。
每个TEST_F必须包含一个注释块,用于说明:
1) 该测试针对crc_seed_value_update的行为检查什么。
2) 该测试验证的内容是什么。
3) 列出传给crc_seed_value_update的输入以及期望的输出。此处的输出包括对mock函数的调用,以及crc_seed_value_update返回的值。
4) 使用提供的测试夹具(test fixture),夹具名为Src_RCrc_RCrcCTest,详见所提供的源代码。
5) 注释块应采用以下格式:
```
/** <测试名称>
*
* <被测内容描述。例如:本测试验证crc_seed_value_update在param1过大时能够检测到。>
*
* 预期结果:
* <结果描述。例如:函数检测到<某输入>超出范围并返回NULL>
*
* 测试条件
* ---------------
* 全局变量初始值:
* <列出本测试用到的全局变量及其初始值。
* 例如:
* GlobalVar1 = 1234>
*
* 函数输入:
* <列出传给crc_seed_value_update的值及期望的返回值。测试中每调用一次
* crc_seed_value_update就记录一条。
* 例如,对一个假设的被测函数(输入2个整数、返回float)调用两次:
* function_name(1234, 5678) -> 2345.0
* function_name( 0, 1) -> 1.23>
*
* 被调用函数及返回值:
* <列出crc_seed_value_update预期会调用的每个函数。每次调用记录一条;若同一函数被多次调用,
* 则该函数会有多条记录。
* 例如:
* CalledFunction1(value1, value2) -> 1234
* CalledFunction2("input string") -> "Returned string"
* CalledFunction1(value3, value4) -> 4321>
*/
TEST_F(<夹具名>,<测试名>)
```
|
4.3 AI生成的单元测试代码示例
从输出中可以观察到以下几点:
- 注释块的格式严格遵循了需求文本中给出的格式。必须如此具体,AI才能稳定地生成一致的输出。
- AI成功地在内存中创建并赋值了一个结构体,用来替换硬编码的R_CRC寄存器地址;不过该结构体是在栈上创建的,而且测试运行结束后AI没有清理R_CRC中保留的指针。这并不是好的做法,但只需调整提示词,就能强制AI采用更好的做法。
- AI正确地设置了初始条件,测试设计也成功地覆盖了switch中CRC_POLYNOMIAL_CRC_8这条路径。对于一个低复杂度函数,AI成功生成了一个像样的单元测试。无论是通过调整提示词,还是由人工评审修改,都可以做一些小幅调整,使测试设计更符合最佳实践。
AI还为该函数以及同一个瑞萨驱动文件中的所有其他函数生成了更多测试用例。生成6000行C++代码和注释,AI只花了大约10分钟和0.8美元的用量费用。如果手工编写同等规模的单元测试,则需要耗费许多个小时。
总体而言,与从测试桩开始手工编写测试相比,“AI生成+人工评审”带来的生产率提升约为3倍。
换句话说,3小时的工作可以压缩到1小时。随着未来更强大的AI出现以及单元测试生成脚本的持续打磨,这一比例还将继续大幅提升。
5.风险管理与安全关键场景的考量
本文介绍的AI辅助单元测试生成同样可以用于医疗设备或航空电子等安全关键应用。必须始终有人参与闭环,评审生成代码的正确性并确保其适用于目标用途。从这个角度看,AI生成的单元测试与使用其他工具生成测试桩并没有本质区别:最终输出仍然由人来评审、修改和签核。
标准软件开发与医疗或其他安全等级软件开发的一个关键区别在于“单元”的定义不同。例如,IEC 62304将单元定义为“不再细分为其他子项的软件项”,但这指的是软件设计层面,而不是代码本身。因此,一个单元通常由一组在逻辑上相关的函数构成,而不像标准软件开发中那样通常指单个函数。由于当前AI系统在处理更大、更复杂的软件时表现不佳,因此需要在设计阶段就把AI的使用纳入考虑,以确保:
- 设计中识别出的单元范围足够小,复杂度不至于高到让AI无法有效地为其编写测试。
- 固件单元测试所采用的测试方法,允许按适合AI的粒度(约100行代码以内)对代码分组进行测试。例如,在单个模块内对调用树中的函数进行增量测试,从而覆盖整棵调用树。
6.投资回报(ROI)——以及适合引入AI的场景
对任何工程流程的变更而言,都必须明确其投资回报(ROI)。AI辅助单元测试生成显著加速了开发工作流,但同时也带来了必须加以管理的相关成本。
这种方法的投入相对较小,但不可或缺:
- 搭建时间:需要投入时间创建基础自动化设施,例如最初的Python脚本和环境配置。
- 提示词工程:开发者必须投入时间创建、测试和打磨迭代式提示词,才能获得高质量的测试代码初稿。
- 修正开销:工程师始终是最后一道人机协同环节,需要花时间评审AI的输出,并纠正其中不可避免出现的严重错误或错误假设。
- AI服务成本:使用AI API的运营费用在整个项目预算中通常微不足道,因此从项目管理层面看是支持成本规避的。
相比之下,收益则相当可观,通常能带来净正的ROI,对较大或长期运行的项目尤其如此:
- 时间缩减:最主要的好处是大幅减少编写测试所需的程序员时间,对于能充分发挥AI优势的函数,往往可比手工测试开发提速3倍。
- 生产率与质量:这种加速还减少了开发者的上下文切换,让他们能专注于更高价值的架构工作。此外,一旦提示词调优到位,AI生成测试的系统性还能带来更高的测试设计整体质量和一致性。
这种方法并非放之四海而皆准,必须考虑其局限性。AI辅助固件单元测试在以下场景中收益最大:
- 新开发项目与规模:如果AI生成器已经搭建完成,那么几乎所有新开发项目都应使用它,因为使用的边际成本极低。而对于尚未具备工具链的项目,则需要达到中大型规模,才值得为搭建AI生成框架进行前期投入。
- 既有代码库:它为给现有代码库快速补充测试提供了一条高效路径,尤其是当这些代码正在进行大量修改时。
- 局限:对于复杂软件,或者包含冗长复杂函数、代码质量欠佳的代码,这项技术的收益会打折扣。此外,极小的团队,或目标硬件受限严重、硬件交互复杂的项目,其收益可能不同或不足以覆盖前期搭建成本。
7.QA工程师角色的演变
实践表明,将AI用于固件单元测试,生产率至少比手工编写测试代码提升3倍。而且随着工具和AI的进步,这种人力的成倍放大效应还会不断增强。需要注意的是,利用AI从根本上改变了人参与代码编写的方式以及所需执行的工作流程。这些流程上的变化带来了新的、不同以往的挑战,焦点转向了如何向AI提供有效输入,以及如何把纠正AI输出所需的人工干预降到最低。
虽然这些挑战是可控的,但遵循本文给出的人机协同AI单元测试实施手册,可以有效应对它们,并让你的测试工作流程至少获得3倍的加速。
如果您正在进行固件单元测试或其他耗时的软件质量保障工作并需要帮助,欢迎随时与我们联系。无论是探索基于AI的自动化方案,还是采用传统路径,我们的团队都乐意为您提供支持。
|