4. 测试平台软件架构分层

车载测试平台要同时面对 CAN/CAN FD、LIN、FlexRay、车载以太网、串口、数字 IO、电源程控、故障注入板卡等异构硬件,还要承载从单条信号读写到整条测试序列的自动化执行。如果所有逻辑写在一个脚本里,换一块板卡、换一个 DBC 或换一种报告格式都会引发大面积返工。分层架构的核心目的,就是把“硬件怎么变”“测试怎么跑”“用例怎么组织”“结果怎么呈现”这四类变化隔离开,让每一层只对相邻层负责。

本节采用五层划分:设备抽象层、驱动层、测试执行层、用例管理层、报告层。需要先明确一个方向约定:调用请求自上而下传递,状态与数据自下而上回传;异常既可以在本层处理,也可以向上抛出,但必须在接口契约中写明。下面这张图给出五层的职责边界、典型对象与关键接口。

车载测试平台五层架构与接口流向 用例管理层 职责:用例建模、参数化、前置/后置条件、用例集与执行顺序编排、版本与标签管理 典型对象:TestCase、TestStep、ParameterSet、Suite、Tag、PreCondition 接口:runSuite(suiteId, env) 回调:onCaseResult(result) 测试执行层 职责:调度执行、步骤解析、断言判定、超时与重试、资源占用与释放、上下文传递 典型对象:Executor、StepRunner、Assertion、Timeout、RetryPolicy、TestContext 接口:sendSignal / readSignal 接口:injectFault / setPower 设备抽象层 职责:屏蔽硬件差异,统一信号/报文/通道/物理量语义,管理 DBC、LDF、ARXML 等描述文件 典型对象:Device、Channel、Signal、Frame、SignalCodec、DatabaseLoader 接口:open/close/configure 接口:writeSignal/readSignal 驱动层 职责:与具体硬件通信,处理帧收发、时序、缓冲区、错误码与底层协议细节 典型对象:VN1640Driver、PCANDriver、LINDriver、EthDriver、DIO/PowerDriver 接口:transmit/receive 接口:getStatus/reset 报告层 职责:结果采集、断言明细、日志与波形关联、统计汇总、多格式导出与归档 典型对象:ResultCollector、ReportModel、HtmlExporter、JUnitExporter、ArtifactStore 接口:collect/export 接口:attachArtifact 调用请求向下 状态数据向上

设备抽象层是“语义层”,它把物理通道上的原始字节翻译成工程意义上的信号。例如 DBC 中定义 VCU_VehSpeed 位于 CAN ID 0x18FEF100,起始位 8,长度 16,因子 0.05625,偏移 0,单位 km/h。抽象层对外只暴露 writeSignal("VCU_VehSpeed", 60.0) 和 readSignal("VCU_VehSpeed"),内部完成缩放、字节序和报文打包。这样测试执行层不需要知道底层是 Vector 还是周立功的硬件。

驱动层是“通信层”,它只关心帧怎么发出去、怎么收回来。以 CAN 为例,驱动层需要处理波特率、采样点、发送队列、接收过滤、总线错误计数等参数。一个典型的驱动配置片段如下:

driver: PCAN_USB
channel: CAN1
baudrate: 500000
samplePoint: 0.8
txQueueSize: 256
rxFilter: [0x18FEF100, 0x18FEF200]
errorFrameLog: true

测试执行层是“动作层”,它把用例管理层下发的步骤翻译成对抽象层的调用序列,并负责断言。例如一个“车速超过 50 km/h 时触发报警”的用例,执行层会先调用 writeSignal("VCU_VehSpeed", 55.0),等待 200 ms,再读取 readSignal("BCM_AlarmSt"),判断是否为 1。如果超时未变化,则按重试策略重发一次,仍失败则记录失败并附带当时的报文日志。

用例管理层是“组织层”,它不直接碰硬件,而是管理用例的元数据:前置条件、参数集、执行顺序、依赖关系、标签和版本。例如一个用例集可以定义为:

suite: 车速报警功能验证
cases:
  - id: TC_SPD_001
    name: 车速55触发报警
    pre: [点火ON, 电源12V]
    params: {speed: 55, waitMs: 200}
    steps: [setSpeed, wait, readAlarm]
    assert: {signal: BCM_AlarmSt, expect: 1}
  - id: TC_SPD_002
    name: 车速45不触发报警
    pre: [点火ON, 电源12V]
    params: {speed: 45, waitMs: 200}
    steps: [setSpeed, wait, readAlarm]
    assert: {signal: BCM_AlarmSt, expect: 0}

报告层是“呈现层”,它接收执行层回传的结果对象,生成可读的报告。一个结果对象至少应包含:用例 ID、步骤序号、时间戳、输入参数、实际值、期望值、判定结果、耗时、关联的日志或波形文件路径。报告层不应参与判定,只做采集、聚合和导出。常见的导出格式包括 HTML、JUnit XML 和 CSV,便于接入 CI 流水线。

接口设计的关键原则:相邻层之间只通过明确定义的接口通信,禁止跨层直接调用。例如用例管理层不能直接调用驱动层的 transmit,必须经过测试执行层和设备抽象层。这样当硬件更换时,只需替换驱动层实现,上层用例和报告格式保持不变。

为了更具体地说明接口契约,下面给出一个设备抽象层的接口定义示例(以伪代码表示):

interface IDevice {
  open(config: DeviceConfig): Result
  close(): void
  configure(params: Map): Result
  writeSignal(name: String, value: Double): Result
  readSignal(name: String): SignalValue
  getStatus(): DeviceStatus
}

interface IDriver {
  transmit(frame: RawFrame): Result
  receive(timeoutMs: Int): RawFrame
  reset(): void
  getErrorCount(): Int
}

在实际工程中,分层带来的收益非常直接:新增一个 LIN 节点时,只需在驱动层增加 LINDriver,在设备抽象层注册对应的 LDF 解析器,测试执行层和用例管理层几乎不需要改动;更换报告模板时,只改报告层的导出器,不影响任何测试逻辑。分层不是目的,而是让变化被限制在最小范围内。

常见误区:把断言逻辑写在报告层,或者让用例管理层直接读取硬件状态。前者会导致报告层无法复用,后者会让用例与硬件强耦合。正确的做法是:断言属于测试执行层,报告层只负责记录和展示。

最后需要强调,五层之间是“契约关系”而非“继承关系”。每一层都可以有多个实现,只要满足接口契约即可替换。例如设备抽象层可以同时支持 CAN 和以太网,测试执行层可以同时调度本地执行器和远程执行器。这种设计让平台在车型迭代和硬件升级时保持稳定。