第四章:自动化测试框架设计

说到自动化测试框架,我见过不少团队上来就写脚本,写到一半发现代码乱成一锅粥。嗯,这其实是个老生常谈的问题——没有框架思维。

今天我们就来聊聊,怎么用Python搭一个靠谱的ECU测试框架。说白了,就是四个核心模块:测试用例管理、报告生成、日志系统、配置文件管理。

4.1 测试用例管理——让用例井井有条

我个人习惯用unittest或pytest来管理测试用例。为什么?因为它们天生支持用例的发现、执行和结果收集。

举个例子,一个典型的ECU测试用例长这样:

import unittest
import can

class TestECUCommunication(unittest.TestCase):
    """ECU通信测试用例"""
    
    def setUp(self):
        # 每个用例执行前的准备工作
        self.bus = can.interface.Bus(channel='can0', bustype='socketcan')
        print("CAN总线已连接")
    
    def tearDown(self):
        # 每个用例执行后的清理工作
        self.bus.shutdown()
        print("CAN总线已断开")
    
    def test_send_receive_message(self):
        """测试发送和接收CAN消息"""
        # 发送一条消息
        msg = can.Message(arbitration_id=0x123, data=[0x11, 0x22, 0x33], is_extended_id=False)
        self.bus.send(msg)
        
        # 接收响应
        response = self.bus.recv(timeout=1.0)
        self.assertIsNotNone(response, "ECU未响应")
        self.assertEqual(response.arbitration_id, 0x456, "响应ID不正确")
    
    def test_error_frame_handling(self):
        """测试错误帧处理"""
        # 模拟错误帧
        error_msg = can.Message(is_error_frame=True)
        self.bus.send(error_msg)
        
        # 验证ECU是否正确处理
        time.sleep(0.5)
        status = self.bus.state
        self.assertEqual(status, can.BusState.ACTIVE, "总线状态异常")

if __name__ == '__main__':
    unittest.main()
我的经验:setUp和tearDown里千万别放耗时操作。我曾经在setUp里初始化整个CAN网络,结果跑100个用例花了半小时。后来改成只连接总线,初始化逻辑单独抽出来,速度提升了10倍。

测试用例的组织结构,我建议按功能模块分文件夹:

test_cases/
├── communication/
│   ├── test_can_bus.py
│   ├── test_lin_bus.py
│   └── test_flexray.py
├── diagnostics/
│   ├── test_uds.py
│   ├── test_obd.py
│   └── test_dtc.py
├── power_management/
│   ├── test_sleep_wakeup.py
│   └── test_voltage_drop.py
└── run_all_tests.py

4.2 测试报告生成——用HTMLTestRunner让结果一目了然

跑完测试,总得给老板看个报告吧?unittest自带的文本报告太简陋了。我推荐用HTMLTestRunner,生成漂亮的HTML报告。

先安装:

pip install html-testrunner

然后这样用:

import unittest
from HTMLTestRunner import HTMLTestRunner

# 发现所有测试用例
test_suite = unittest.TestLoader().discover('test_cases', pattern='test_*.py')

# 配置报告
report_file = open('test_report.html', 'wb')
runner = HTMLTestRunner(
    stream=report_file,
    title='ECU自动化测试报告',
    description='测试环境:CANoe v11.0 | 测试时间:2024-01-15',
    verbosity=2
)

# 执行测试并生成报告
result = runner.run(test_suite)
report_file.close()

报告里会包含什么?

  • 测试通过/失败/错误的数量统计
  • 每个用例的执行时间
  • 失败用例的详细错误信息
  • 测试环境的描述信息
  • 漂亮的进度条和饼图
注意:HTMLTestRunner默认只支持Python 3.x的某些版本。如果你用的是Python 3.10以上,可能需要找社区修改版。我曾经在这上面踩过坑,折腾了半天才发现是版本兼容问题。

4.3 日志系统——用logging模块记录一切

测试跑失败了,总得知道为什么吧?日志就是你的救命稻草。

Python自带的logging模块其实很强大,关键是要配置好:

import logging
import logging.handlers

def setup_logger():
    """配置日志系统"""
    # 创建logger
    logger = logging.getLogger('ECU_Test')
    logger.setLevel(logging.DEBUG)
    
    # 文件处理器:记录所有日志
    file_handler = logging.handlers.RotatingFileHandler(
        'test_log.log',
        maxBytes=10*1024*1024,  # 10MB
        backupCount=5,
        encoding='utf-8'
    )
    file_handler.setLevel(logging.DEBUG)
    
    # 控制台处理器:只显示INFO及以上
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)
    
    # 定义格式
    formatter = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )
    file_handler.setFormatter(formatter)
    console_handler.setFormatter(formatter)
    
    # 添加处理器
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)
    
    return logger

# 使用示例
logger = setup_logger()
logger.info("开始执行CAN通信测试")
logger.debug("发送消息: ID=0x123, Data=[0x11, 0x22, 0x33]")
logger.warning("响应超时,重试第2次")
logger.error("ECU无响应,测试失败")
我的习惯:日志级别要分层。DEBUG记录所有细节(比如每条CAN消息),INFO记录关键步骤,WARNING记录异常但不影响流程的情况,ERROR记录真正的失败。这样排查问题时,先看ERROR,再根据时间戳看DEBUG,效率高很多。

4.4 配置文件管理——用configparser告别硬编码

你想想看,每次换测试环境都要改代码里的CAN通道、波特率、ECU地址...是不是很烦?

用configparser把配置抽出来,一劳永逸:

import configparser
import os

class TestConfig:
    """测试配置管理类"""
    
    def __init__(self, config_file='test_config.ini'):
        self.config = configparser.ConfigParser()
        
        # 检查配置文件是否存在
        if not os.path.exists(config_file):
            self.create_default_config(config_file)
        
        self.config.read(config_file, encoding='utf-8')
    
    def create_default_config(self, config_file):
        """创建默认配置文件"""
        self.config['CAN_BUS'] = {
            'channel': 'can0',
            'bustype': 'socketcan',
            'bitrate': '500000'
        }
        self.config['ECU'] = {
            'address': '0x7E0',
            'response_timeout': '1.0',
            'retry_count': '3'
        }
        self.config['TEST'] = {
            'log_level': 'INFO',
            'report_path': './reports',
            'max_test_time': '3600'
        }
        
        with open(config_file, 'w', encoding='utf-8') as f:
            self.config.write(f)
        print(f"默认配置文件已创建: {config_file}")
    
    def get_can_config(self):
        """获取CAN总线配置"""
        return {
            'channel': self.config.get('CAN_BUS', 'channel'),
            'bustype': self.config.get('CAN_BUS', 'bustype'),
            'bitrate': int(self.config.get('CAN_BUS', 'bitrate'))
        }
    
    def get_ecu_config(self):
        """获取ECU配置"""
        return {
            'address': int(self.config.get('ECU', 'address'), 16),
            'timeout': float(self.config.get('ECU', 'response_timeout')),
            'retry': int(self.config.get('ECU', 'retry_count'))
        }

# 使用示例
config = TestConfig()
can_cfg = config.get_can_config()
ecu_cfg = config.get_ecu_config()

bus = can.interface.Bus(**can_cfg)
print(f"CAN总线已配置: {can_cfg}")
print(f"ECU地址: 0x{ecu_cfg['address']:X}")

配置文件长这样:

[CAN_BUS]
channel = can0
bustype = socketcan
bitrate = 500000

[ECU]
address = 0x7E0
response_timeout = 1.0
retry_count = 3

[TEST]
log_level = INFO
report_path = ./reports
max_test_time = 3600
避坑指南:我曾经把配置文件里的波特率写成了50000(少了个0),结果CAN通信死活不通。排查了整整一天才发现是配置问题。所以建议在读取配置后,加一个校验函数,确保关键参数在合理范围内。

4.5 整合起来——一个完整的测试框架示例

好了,四个模块都讲完了。我们来把它们整合成一个完整的测试脚本:

import unittest
import logging
from HTMLTestRunner import HTMLTestRunner
from test_config import TestConfig

def main():
    # 1. 加载配置
    config = TestConfig()
    log_level = config.config.get('TEST', 'log_level')
    
    # 2. 配置日志
    logging.basicConfig(
        level=getattr(logging, log_level),
        format='%(asctime)s - %(levelname)s - %(message)s',
        handlers=[
            logging.FileHandler('test_run.log', encoding='utf-8'),
            logging.StreamHandler()
        ]
    )
    logger = logging.getLogger(__name__)
    logger.info("测试框架启动")
    
    # 3. 发现并执行测试用例
    test_suite = unittest.TestLoader().discover('test_cases', pattern='test_*.py')
    
    # 4. 生成报告
    report_path = config.config.get('TEST', 'report_path')
    os.makedirs(report_path, exist_ok=True)
    report_file = open(f'{report_path}/ecu_test_report.html', 'wb')
    
    runner = HTMLTestRunner(
        stream=report_file,
        title='ECU自动化测试报告',
        description=f'配置文件: test_config.ini | 日志级别: {log_level}'
    )
    
    result = runner.run(test_suite)
    report_file.close()
    
    logger.info(f"测试完成: 通过{result.testsRun - len(result.failures) - len(result.errors)}个, "
                f"失败{len(result.failures)}个, 错误{len(result.errors)}个")
    
    return result

if __name__ == '__main__':
    main()

总结一下:

  • 测试用例管理:用unittest/pytest,按功能模块组织
  • 报告生成:HTMLTestRunner,漂亮直观
  • 日志系统:logging模块,分级记录
  • 配置文件:configparser,灵活可配

这四个模块搭起来,就是一个能打硬仗的测试框架了。

嗯,框架设计这块就聊到这儿。下一章我们讲讲怎么用Python操作CANoe和CANalyzer,那可是ECU测试的硬核内容。