第三章 测试脚本设计:测试用例结构、测试分组、测试断言、测试日志输出
好,咱们进入第三章。这一章我打算聊聊测试脚本怎么写才靠谱。说实话,我见过太多测试脚本写得跟天书似的——自己写的代码,过两周再看,自己都看不懂。嗯,咱们今天就把这事儿掰扯清楚。
3.1 测试用例结构:别让代码变成一团乱麻
先说说测试用例的结构。我个人习惯把每个测试用例看作一个独立的小故事——有开头、有过程、有结尾。在CANoe里,我通常用CAPL或者配合Python来写。
一个标准的测试用例结构,我建议分成这么几块:
- 前置条件:测试开始前,系统得处于什么状态?比如总线速率、节点状态、信号初始值。
- 测试步骤:你具体要做什么操作?发送什么报文?等待多长时间?
- 预期结果:你希望看到什么?信号值变化?DTC报出?还是某个响应报文出现?
- 后置处理:测试完了,怎么恢复环境?别让下一个测试用例受影响。
我在项目中遇到过最头疼的事,就是测试用例之间互相干扰。比如第一个用例把某个节点设成了休眠模式,第二个用例上来就发诊断请求,结果超时了。你说这是用例2的bug吗?其实不是,是环境没恢复好。
核心原则:每个测试用例必须独立运行,互不依赖。这是自动化测试的底线。
下面给个简单的CAPL示例,你看看结构:
testcase TC_CAN_Signal_Check()
{
// 前置条件
TestWaitForKeypress("请确保总线连接正常,按任意键继续...");
// 测试步骤
CAN_1.SetSignal("EngineSpeed", 1500);
TestWaitForTimeout(100); // 等待100ms
// 测试断言
if (CAN_1.GetSignal("EngineSpeed") == 1500)
{
TestStepPass("发动机转速设置正确");
}
else
{
TestStepFail("发动机转速设置失败,期望1500,实际" + CAN_1.GetSignal("EngineSpeed"));
}
// 后置处理
CAN_1.SetSignal("EngineSpeed", 0);
}
3.2 测试分组:把大象装进冰箱需要几步?
测试用例多了以后,你得学会分组。说白了,就是把相关的测试用例放在一起。我一般按这么几种维度来分:
| 分组维度 | 举例 | 适用场景 |
|---|---|---|
| 按功能模块 | 车窗控制、车门锁、灯光系统 | 功能验证阶段 |
| 按测试类型 | 功能测试、压力测试、边界测试 | 回归测试阶段 |
| 按总线类型 | CAN测试、LIN测试、FlexRay测试 | 多总线项目 |
| 按优先级 | P0(阻塞)、P1(重要)、P2(一般) | 冒烟测试 |
在CANoe里,你可以用Test Module的文件夹结构来分组。我个人习惯用Python脚本动态生成测试列表,这样更灵活。你想想看,如果项目有2000个测试用例,手动分组得累死。
小技巧:我建议在测试用例命名时就带上分组信息。比如 TC_Window_Lift_Up_Down,一看就知道是车窗升降的测试。别用 TC_001、TC_002 这种,过两天你就忘了001是测啥的。
3.3 测试断言:别模棱两可,要一针见血
测试断言是什么?说白了就是判断测试通过还是不通过的标准。这里我踩过不少坑,跟大家分享几个原则。
原则一:断言要精确
我曾经见过有人这么写断言:
if (signal_value > 0)
{
TestStepPass("信号值大于0");
}
这有啥问题?你想想看,如果信号值应该是100,结果测出来是1,它也大于0,测试通过了。但实际功能是错的!所以断言一定要精确到具体值或者具体范围。
原则二:不要只断言一次
一个测试用例里,往往需要多个断言。比如测一个车窗升降功能:
- 断言1:发送上升指令后,车窗位置信号增加
- 断言2:到达顶部后,电机电流下降
- 断言3:防夹功能触发时,车窗反向运动
每个关键步骤都要有断言,别偷懒。
原则三:断言要有明确的失败信息
我最烦看到这种断言失败信息:
TestStepFail("测试失败");
失败在哪?期望值是多少?实际值是多少?全没有。排查问题得从头看日志,浪费时间。我建议这么写:
TestStepFail("车窗上升超时,期望3秒内到达顶部,实际耗时" + actual_time + "秒");
避坑指南:我曾经在一个项目里,因为断言写得太模糊,导致一个bug在生产线上跑了三个月才被发现。那是一个关于车速信号有效性的断言,只判断了"信号存在",没判断"信号值是否在合理范围内"。结果车速信号一直显示0,但测试全通过了。嗯,从那以后,我对断言的要求就变得特别苛刻。
3.4 测试日志输出:让问题无处遁形
日志输出,说白了就是给未来的自己留线索。我见过太多测试脚本,日志就两行:
测试开始
测试结束
这有啥用?出了问题你根本不知道中间发生了什么。
我建议日志输出要包含这些信息:
- 时间戳:精确到毫秒,方便定位时序问题
- 步骤编号:第几步做了什么
- 关键数据:发送了什么报文,收到了什么响应
- 断言结果:每个断言是Pass还是Fail,带上期望值和实际值
- 异常信息:如果出现异常,把堆栈信息也打出来
在CANoe里,我一般用 write() 函数输出日志,配合 TestStepPass() 和 TestStepFail() 来记录断言结果。Python脚本的话,我习惯用 logging 模块,设置不同的日志级别。
给你看个Python的日志示例:
import logging
import datetime
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
filename='test_log_' + datetime.datetime.now().strftime('%Y%m%d_%H%M%S') + '.log'
)
def test_step(step_name, result, expected, actual):
if result:
logging.info(f"[PASS] {step_name} - 期望: {expected}, 实际: {actual}")
else:
logging.error(f"[FAIL] {step_name} - 期望: {expected}, 实际: {actual}")
# 使用示例
test_step("车窗上升测试", signal_value == 100, 100, signal_value)
你看,这样日志里就清清楚楚:什么时间、哪个步骤、期望什么、实际是什么。排查问题的时候,直接搜FAIL就能找到所有失败点。
我的经验:日志文件最好按日期和测试轮次命名,比如 test_log_20240115_round3.log。这样回溯问题的时候,能快速找到对应的日志。别用 test.log 这种名字,覆盖了就找不回来了。
好了,这一章的内容就到这儿。测试脚本设计这事儿,说白了就是三个字:结构化。用例结构清晰、分组合理、断言精确、日志完整,你的自动化测试就成功了一大半。下一章咱们聊聊测试报告怎么生成,那才是真正出活儿的地方。