ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI辅助测试开发:从一句话需求到可执行测试序列的落地实践

AI辅助测试开发:从一句话需求到可执行测试序列的落地实践 1. 从“一句话需求”到测试序列中间到底隔着什么测试开发这行干久了你会发现一个特别有意思的现象产品经理或者项目负责人丢过来一句话——“帮我测一下这个设备的通信功能正不正常”然后就没下文了。这句话听起来简单但真正落到测试开发工程师手里它需要被翻译成一套可执行、可复现、可判定的测试序列。这个过程在过去完全靠人肉完成而现在AI辅助测试开发正在改变这个翻译过程。所谓“一句话需求”本质上是一个高度模糊的自然语言描述。它没有指定通信协议、没有说明测试边界、没有定义通过标准、更没有给出异常场景的覆盖范围。而“可执行测试序列”则要求每一步都有明确的指令、参数、预期响应和超时处理。这两者之间的鸿沟就是AI辅助测试开发要填的坑。我所在的团队主要做工业设备和嵌入式系统的自动化测试日常打交道的协议包括SCPI、Modbus RTU、Modbus TCP、串口通信等。这些协议各有各的脾气SCPI偏向仪器控制命令是ASCII字符串结构相对规整Modbus则是二进制协议分线圈、离散输入、保持寄存器、输入寄存器四种数据区功能码从01到16各有各的语义。过去我们写测试用例一个中等复杂度的设备通信测试从需求理解到脚本跑通大概需要两到三天。引入AI辅助之后这个周期压缩到了半天左右而且覆盖的场景反而更全了。这篇文章适合几类人看一是正在做自动化测试但还没接触AI辅助的工程师想了解这条路到底能不能走通二是团队管理者在评估要不要把AI工具引入测试流程三是对SCPI、Modbus这些协议不太熟悉但需要快速上手写测试序列的开发者。我会把整个落地过程拆开讲包括需求解析、协议映射、序列生成、执行验证这几个关键环节每个环节都会说清楚为什么这么做、怎么做、以及我踩过哪些坑。2. 需求解析AI怎么把一句模糊的话拆成测试意图2.1 为什么不能让AI直接生成测试脚本很多人第一反应是既然有大模型为什么不直接把“帮我测一下这个设备的通信功能”丢进去让它输出一段pytest脚本我试过结果惨不忍睹。大模型会给你生成一段看起来很像那么回事的代码但里面的寄存器地址是编的功能码是猜的超时时间是拍脑袋定的。原因很简单大模型不知道你的设备手册不知道你的通信参数更不知道你的测试环境拓扑。所以正确的做法不是让AI一步到位生成脚本而是让它先做需求解析把模糊的自然语言拆解成结构化的测试意图。测试意图是一组明确的、可验证的断言集合它不涉及具体协议细节只描述“要验证什么”。比如“通信功能正常”可以被拆解为设备能响应基本查询命令、能正确读写指定数据区、能在异常帧下返回错误码、能在超时后恢复通信。这个拆解过程AI的优势在于它能穷举你可能忽略的边界场景。人写测试用例容易陷入“ happy path ”思维只测正常流程。AI会提醒你要不要测一下功能码非法的情况要不要测一下数据长度超限的情况要不要测一下广播地址的响应行为这些提醒不一定都对但至少给你一个检查清单。2.2 从测试意图到协议映射的关键转换测试意图拆出来之后下一步是协议映射。这一步是AI辅助测试开发里最需要人工介入的环节也是最容易出错的环节。以Modbus为例“读取设备保持寄存器”这个测试意图映射到具体协议时需要考虑从站地址是多少起始寄存器地址是多少读取多少个寄存器用功能码03还是04超时时间设多少这些参数AI没法凭空知道必须从设备手册或者现有配置中提取。我的做法是维护一个“设备能力描述文件”用YAML格式记录每个设备支持的协议类型、通信参数、数据区映射关系。这个文件不需要很复杂但必须准确。AI在生成测试序列时会引用这个文件里的参数而不是自己编造。这个文件本身也可以让AI辅助生成初稿但必须经过人工核对。device: name: power_meter_01 protocol: modbus_tcp ip: 192.168.1.100 port: 502 slave_id: 1 registers: voltage: address: 0x0000 type: holding data_type: uint16 scale: 0.1 unit: V current: address: 0x0001 type: holding data_type: uint16 scale: 0.01 unit: A timeout_ms: 1000 retry: 2有了这个描述文件AI就能把“读取电压和电流”这个测试意图转换成具体的Modbus TCP请求序列。这里有个经验描述文件里的地址一定要用十六进制和十进制同时标注因为不同手册的写法不一样AI在解析时容易搞混。我就在这个问题上栽过跟头AI把手册里的“40001”直接当成了寄存器偏移量实际上Modbus的保持寄存器地址40001对应的偏移量是0x0000这个转换关系必须显式告诉AI。2.3 测试序列的结构化表达测试意图和协议参数都齐了之后AI需要把它们组织成可执行的测试序列。我采用的是一种中间表示格式既不是自然语言也不是最终代码而是一种结构化的JSON描述。这个中间层的好处是它跟具体测试框架解耦今天用pytest明天换Robot Framework只需要换一个转换器就行。一个典型的测试序列中间表示长这样{ test_case: modbus_read_voltage_current, description: 验证设备电压电流读取功能, preconditions: [ {action: connect, target: power_meter_01}, {action: set_timeout, value: 1000} ], steps: [ { action: read_holding_registers, slave_id: 1, start_address: 0x0000, quantity: 2, expected: { function_code: 3, byte_count: 4, voltage_range: [0, 300], current_range: [0, 50] } } ], postconditions: [ {action: disconnect} ], tags: [smoke, modbus, read] }这个中间表示的好处是AI生成的内容是可审查的。你可以一眼看出它读了哪个地址、读了多少个、预期范围是多少。如果AI搞错了你在这一步就能发现而不是等到脚本跑起来报一堆看不懂的异常。3. 协议细节SCPI和Modbus在AI生成序列中的处理差异3.1 SCPI命令的文本特性与AI生成策略SCPI协议跟Modbus完全是两个路子。SCPI是文本协议命令长得像英文句子比如“MEASure:VOLTage:DC?”就是测量直流电压。这种文本特性对AI来说反而更友好因为大模型在训练数据里见过大量的SCPI命令示例它生成的命令格式通常不会太离谱。但SCPI的坑在于它的命令树结构。SCPI命令有长格式和短格式之分比如“MEASure”可以简写成“MEAS”但简写规则不是简单的截断而是有特定规则的。AI有时候会过度简写把“MEASure”写成“MEA”这就错了。我的处理方式是在提示词里明确给出命令树的完整定义让AI从预定义的命令集中选择而不是自由发挥。另一个坑是SCPI的查询响应格式。不同厂商的仪器对同一条查询命令的响应格式可能不一样有的返回“1.234E01”有的返回“1.234”有的还带单位。AI在生成预期响应时如果不知道具体仪器的响应格式就会生成一个过于严格的断言导致测试误报。我的做法是在设备描述文件里增加一个“response_format”字段用正则表达式描述响应格式AI根据这个正则来生成断言。# SCPI查询响应解析示例 import re def parse_scpi_response(response: str, pattern: str) - float: 根据预定义的正则模式解析SCPI响应 pattern示例: r([-]?\d\.?\d*)(?:E([-]?\d))? match re.search(pattern, response.strip()) if not match: raise ValueError(f响应格式不匹配: {response}) value float(match.group(1)) if match.group(2): value * 10 ** int(match.group(2)) return value3.2 Modbus二进制协议的字节级陷阱Modbus是二进制协议AI在处理字节级细节时明显不如处理文本那么得心应手。最常见的问题是字节序。Modbus规范里寄存器是16位的但一个32位浮点数需要两个寄存器来存储。这两个寄存器谁在前谁在后规范没有强制规定不同设备厂商的实现可能不一样。AI如果不知道具体设备的字节序生成的解析代码就会读出乱七八糟的值。我的解决方案是在设备描述文件里显式声明字节序并且让AI生成两种解析方式的代码然后在实际设备上跑一遍看哪种能读出合理值。这个方法听起来笨但比反复猜要快得多。import struct def parse_float32(registers: list, byte_order: str big) - float: 解析32位浮点数 registers: 两个16位寄存器的值 byte_order: big 或 little if byte_order big: raw struct.pack(HH, registers[0], registers[1]) else: raw struct.pack(HH, registers[1], registers[0]) return struct.unpack(f, raw)[0]还有一个坑是Modbus的错误码处理。Modbus协议规定当从站返回异常时功能码的最高位会被置1后面跟一个异常码。比如功能码03的正常响应是0x03异常响应是0x83后面跟异常码。AI在生成测试序列时如果只考虑了正常响应就会漏掉异常场景的测试。我在提示词里明确要求AI必须为每个功能码生成至少一个异常场景测试包括非法功能码、非法数据地址、非法数据值、从站设备故障这四种标准异常。3.3 两种协议在测试序列生成中的统一抽象虽然SCPI和Modbus差异很大但在测试序列的中间表示层它们可以被统一抽象。我定义了几种基本的操作类型connect、disconnect、send_command、read_response、assert_response、wait、retry。SCPI的“发送查询命令并解析响应”和Modbus的“读取保持寄存器并解析数据”都可以映射到这几个基本操作的组合上。这种统一抽象的好处是测试序列的生成逻辑可以复用。AI只需要学会如何把测试意图映射到基本操作序列而不需要为每种协议单独学习一套生成规则。当需要支持新协议时只需要增加一个协议适配器把新协议的命令映射到基本操作上就行。操作类型SCPI映射Modbus映射connect打开VISA资源建立TCP连接或打开串口send_command发送SCPI命令字符串构造并发送Modbus PDUread_response读取直到换行符读取固定长度或根据功能码计算assert_response正则匹配或数值范围判断功能码校验数据范围判断disconnect关闭VISA资源关闭TCP连接或串口这个表格是我在实际项目中总结出来的它帮助我理清了不同协议在测试序列层面的共性。AI在生成测试序列时我会把这个表格作为上下文提供给它让它按照这个映射关系来生成而不是自己发明一套结构。4. 从中间表示到可执行代码生成、审查与修正4.1 pytest框架下的代码生成模板中间表示确定之后下一步是把它转换成可执行的测试代码。我们团队主要用pytest所以转换器是围绕pytest写的。转换器的核心逻辑是遍历测试序列的每一步根据操作类型选择对应的代码模板然后把参数填充进去。# 转换器核心逻辑示例 def generate_pytest_code(test_sequence: dict) - str: lines [] lines.append(import pytest) lines.append(from modbus_client import ModbusClient) lines.append() lines.append(fdef test_{test_sequence[test_case]}():) for step in test_sequence[steps]: if step[action] read_holding_registers: lines.append(f client ModbusClient({step[slave_id]})) lines.append(f result client.read_holding_registers() lines.append(f {step[start_address]}, {step[quantity]})) lines.append(f assert result.function_code {step[expected][function_code]}) lines.append(f assert {step[expected][voltage_range][0]} result.voltage {step[expected][voltage_range][1]}) return \n.join(lines)这个转换器本身不复杂复杂的是处理各种边界情况。比如超时重试逻辑、连接失败后的清理逻辑、多个测试步骤之间的状态依赖等。这些逻辑如果让AI直接生成很容易出现资源泄漏或者状态污染。我的做法是把这些通用逻辑封装成pytest的fixtureAI生成的代码只需要调用fixture不需要自己管理连接生命周期。4.2 AI生成代码的审查要点AI生成的测试代码我从来不会直接合并到主分支。必须经过一轮人工审查重点看几个地方一是断言是否过于宽松或过于严格过于宽松会导致漏报过于严格会导致误报二是异常处理是否完整特别是连接断开、超时、协议错误这些场景三是资源释放是否可靠有没有可能在断言失败时跳过清理步骤。我总结了一个审查清单每次审查AI生成的代码时逐条过每个测试用例是否有明确的通过/失败判定条件超时时间是否合理是否考虑了设备响应慢的情况是否覆盖了至少一个异常场景连接和断开是否成对出现是否用了try/finally或fixture寄存器地址和功能码是否与设备手册一致字节序和数据类型是否与设备实现一致测试用例之间是否有状态依赖是否可独立运行这个清单看起来简单但实际审查时能发现不少问题。有一次AI生成了一个读取保持寄存器的测试功能码用了03但设备手册上写的是04。03是读保持寄存器04是读输入寄存器虽然都是读操作但数据区不一样读出来的值完全不同。这种错误如果没审查出来测试跑通了也是假的。4.3 实测中的意外情况与修正策略即使经过了审查实测阶段还是会遇到意外。我印象最深的一次是测试一个Modbus RTU设备AI生成的序列里设置了100ms的超时结果在实际设备上频繁超时。排查后发现这个设备的响应时间在波特率9600的情况下平均要150ms左右100ms根本不够。AI之所以设100ms是因为它在训练数据里看到的Modbus超时大多是100ms到200ms它取了一个偏小的值。这个问题的修正策略不是简单地把超时改大而是要在设备描述文件里增加一个“response_time_profile”字段记录设备在不同波特率下的典型响应时间。AI在生成超时参数时会参考这个字段而不是拍脑袋决定。另一个意外是Modbus TCP的并发连接问题。AI生成的测试序列默认是串行执行的但有些测试场景需要模拟多个主站同时访问同一个从站。AI一开始生成的代码是顺序建立多个连接但没考虑连接之间的干扰。后来我在中间表示里增加了“concurrent”标记转换器会根据这个标记生成多线程或异步的测试代码。5. 测试序列的验证与迭代怎么知道AI生成的东西是对的5.1 用已知设备做基准验证AI生成的测试序列到底靠不靠谱最直接的验证方法是在已知设备上跑一遍。我们团队维护了几台“黄金设备”这些设备的通信行为已经被人工验证过响应时间、数据格式、异常行为都有详细记录。每次AI生成新的测试序列先在这几台设备上跑如果结果与预期一致才考虑推广到其他设备。这个做法看起来费事但实际上是省时间的。因为AI生成的序列如果直接在产线设备上跑出了问题排查成本很高而且可能影响生产。用黄金设备做基准验证相当于给AI的输出加了一道质量门禁。5.2 测试序列的覆盖率评估除了单条测试序列的正确性还需要评估AI生成的测试序列集合是否覆盖了足够的场景。我用的方法是对比人工编写的测试用例集和AI生成的测试用例集看两者的覆盖差异。具体来说我会把测试意图分类比如“基本读写”“边界值”“异常处理”“并发访问”“长时间稳定性”然后统计每个类别下人工和AI各自覆盖了多少。实测下来AI在“异常处理”和“边界值”这两个类别上的覆盖率明显高于人工。原因很简单人写测试用例时容易忽略不常见的异常场景而AI会穷举所有可能的异常码和边界条件。但在“长时间稳定性”和“并发访问”这两个类别上AI的覆盖反而不如人工因为这两类测试需要理解系统的实际运行环境AI缺乏这方面的上下文。所以我的策略是让AI负责生成基础的功能测试和异常测试人工负责补充稳定性测试和并发测试。两者结合覆盖率和效率都能兼顾。5.3 持续迭代把实测反馈喂回给AIAI辅助测试开发不是一次性的工作而是一个持续迭代的过程。每次实测中发现的误报、漏报、超时、解析错误都应该被记录下来作为反馈喂回给AI。我的做法是维护一个“测试序列修正日志”记录每次修正的原因和修正后的结果。这个日志会作为上下文的一部分在下次生成测试序列时提供给AI。# 测试序列修正日志 ## 2024-01-15 - 设备: power_meter_01 - 问题: AI生成的超时时间100ms过短实际响应时间约150ms - 修正: 超时时间改为500ms并在设备描述文件中增加response_time_profile - 影响: 所有使用该设备的测试序列超时参数自动更新 ## 2024-01-18 - 设备: temperature_controller_03 - 问题: AI使用了功能码03读取输入寄存器实际应为04 - 修正: 在设备描述文件中明确标注每个数据区对应的功能码 - 影响: 后续生成的测试序列不再出现功能码错误这个日志看起来琐碎但积累下来就是团队的宝贵资产。新来的工程师接手时看这个日志就能快速了解哪些地方容易出错避免重复踩坑。6. 落地过程中的几个关键决策点6.1 为什么选择中间表示而不是直接生成代码这个决策我在前面提过但值得再展开说一下。直接让AI生成pytest代码最大的问题是不可审查。代码里混杂了业务逻辑、协议细节、框架API审查者很难快速判断哪部分是对的哪部分是错的。而中间表示把测试意图和协议参数分离审查者可以分别检查“测试意图是否完整”和“协议参数是否正确”审查效率高很多。另一个好处是可移植性。中间表示跟测试框架无关今天用pytest明天换Robot Framework或者自研框架只需要换一个转换器。如果直接生成pytest代码换框架时所有测试序列都要重新生成。6.2 人工介入的边界在哪里AI辅助测试开发不是完全无人化人工介入的边界需要明确。我的经验是需求解析和测试意图拆解阶段AI可以主导人工审核协议映射阶段人工主导AI辅助测试序列生成阶段AI主导人工审查实测验证阶段人工主导AI辅助分析日志。这个分工的核心逻辑是AI擅长穷举和模式匹配人擅长判断和决策。让AI做它擅长的事人做判断和兜底整体效率最高。6.3 团队协作中的提示词管理如果团队多人使用AI辅助测试开发提示词的管理就很重要。不同人写的提示词风格不一样生成的测试序列质量也参差不齐。我的做法是建立一套提示词模板库把常用的提示词固化下来包括需求解析模板、协议映射模板、序列生成模板、异常场景补充模板等。每个人可以根据具体情况微调但核心结构保持一致。提示词模板库还需要版本管理。每次发现新的坑或者新的最佳实践就更新模板并在团队内同步。这样整个团队的AI使用水平会逐渐趋同不会出现某个人用得好、某个人用得差的情况。7. 我踩过的几个坑和对应的解决方案第一个坑是过度信任AI生成的寄存器地址。有一次AI生成了一个读取电压的测试地址写的是0x0000但实际设备手册上电压的地址是0x00010x0000是保留地址。测试跑起来没报错但读出来的值是0。这个坑的教训是AI生成的地址必须跟设备手册逐条核对不能因为测试跑通了就认为是对的。第二个坑是忽略了Modbus TCP的事务标识符。Modbus TCP的报文头里有一个事务标识符字段用于匹配请求和响应。AI生成的测试代码在单连接串行执行时没问题但在并发场景下事务标识符不匹配会导致响应错乱。解决方案是在中间表示里增加事务标识符的管理逻辑确保每个请求都有唯一的事务ID。第三个坑是SCPI命令的大小写敏感问题。SCPI规范说命令不区分大小写但有些仪器实现是区分大小写的。AI生成的命令有时候全大写有时候全小写在区分大小写的仪器上就会失败。解决方案是在设备描述文件里增加“case_sensitive”字段AI根据这个字段决定生成大写还是小写命令。第四个坑是测试序列的执行顺序依赖。AI生成的测试序列有时候会假设前一个测试已经执行过比如先写寄存器再读寄存器。但如果单独执行读寄存器的测试就会失败。解决方案是在中间表示里显式声明测试之间的依赖关系转换器根据依赖关系生成setup和teardown逻辑。8. 这套方法在实际项目中的效果我们团队用这套方法跑了大概半年覆盖了十几种工业设备和嵌入式模块的通信测试。最直观的变化是测试用例的编写效率提升了三到四倍原来两三天的工作量现在半天就能完成。更重要的是测试覆盖率上去了特别是异常场景的覆盖从原来的人工覆盖百分之三四十提升到了百分之七八十。当然也有代价。前期搭建中间表示、转换器、设备描述文件、提示词模板库花了大概两周时间。这两周里没有产出任何测试用例全在搭基础设施。但从第三周开始效率优势就体现出来了。所以我的建议是如果只是临时测一两个设备不值得搭这套东西如果是长期做设备通信测试这套基础设施的投入是值得的。还有一个意外收获是测试序列的可读性变好了。以前人工写的测试脚本不同人风格不一样有的用面向对象有的用函数式维护起来很头疼。现在所有测试序列都从中间表示生成风格统一新人接手时看中间表示就能理解测试意图不需要去啃代码。最后分享一个我在实际操作中的小技巧在设备描述文件里加一个“known_issues”字段记录这个设备已知的通信怪癖。比如某个设备的Modbus实现不支持功能码03读取超过10个寄存器超过就返回异常。AI在生成测试序列时会参考这个字段避免生成超出设备能力的测试。这个字段不需要很正式用自然语言描述就行AI能理解。
返回列表