CAPL Script

如何编写一个测试模块

学习目标

完成本篇后,你将能够:

  • 理解 CAPL 测试模块(Test Module)的基本概念和架构
  • 掌握测试模块的核心组件:testcaseMainTest、测试步骤(Test Step)
  • 学会使用测试服务库(TSL)的检查(Checks)和激励(Stimulus)功能
  • 独立创建一个完整的测试模块并生成测试报告
  • 对整个教程系列的最终项目有清晰的认识

从仿真到测试的跨越

在前面的学习中,我们已经掌握了如何使用 CAPL 创建仿真节点(Simulation Node)。仿真节点能够模拟真实的 ECU,发送消息、响应事件,就像真的硬件一样工作。

但是,作为一名汽车电子工程师,你可能会问:

"我的 ECU 代码写完了,仿真也跑通了。但我怎么知道它真的按照需求工作?有没有 bug?"

这时候,你就需要从仿真转向测试(Test)。CAPL 提供了专门的测试模块(Test Module)功能,让你能编写自动化测试用例,验证 ECU 的行为是否符合预期。

简单来说:

  • 仿真节点 = "假装"其他 ECU 存在
  • 测试模块 = "验证"我的 ECU 正确工作

什么是测试模块?

测试模块(Test Module)是 CAPL 中用于自动化测试的特殊程序类型。与仿真节点不同,测试模块不参与总线通信,而是:

  1. 发送激励(Stimulus):向被测 ECU 发送测试命令
  2. 检查响应(Check):验证 ECU 的行为是否符合预期
  3. 生成报告(Report):自动记录测试结果

测试模块的核心组件

一个完整的测试模块包含以下部分:

1. testcase(测试用例)

testcase 是测试的基本单元,定义一个完整的测试场景:

testcase TC_DoorLockTest()
{
    // Test implementation
}

2. MainTest(主测试函数)

MainTest 是测试模块的入口点,负责调度和组织所有测试用例:

void MainTest()
{
    // Call test cases
    TC_DoorLockTest();
    TC_WindowControlTest();
}

3. 测试步骤(Test Step)

测试步骤是测试用例中的最小单元,用于记录和判定测试过程:

TestStep(0, "Step 1", "Send lock command");
TestStepPass("Lock command sent successfully");

4. 测试服务库(TSL)

TSL 提供了丰富的测试工具函数,包括:

  • Checks(检查):验证信号、消息、时间等
  • Stimulus(激励):生成测试数据、模拟信号变化

创建第一个测试模块

让我们从一个简单的例子开始,测试车门锁功能。

步骤 1:新建测试模块文件

  1. 在 CAPL Browser 中,点击 FileNewCAPL Test Module File
  2. 系统会创建一个包含基本结构的测试模块文件

步骤 2:编写 MainTest 函数

首先,我们创建一个空的 MainTest 函数框架:

void MainTest()
{
    // Step 1: Call test cases

    // Step 2: Add more test cases here
}

步骤 3:创建第一个测试用例

现在,我们添加一个测试用例:

testcase TC_DoorLockTest()
{
    // This test case verifies door lock functionality
}

步骤 4:添加测试步骤

接下来,我们完善这个测试用例:

variables
{
    byte doorStatus = 0;  // Variable to store received door status
}

// Handler to update doorStatus when DoorStatus message is received
on message 0x300  // DoorStatus message
{
    doorStatus = this.byte(0);  // Update door status from message
}

testcase TC_DoorLockTest()
{
    // Step 1: Set test case title and description
    TestCaseTitle("TC 1.0", "Door Lock Function Test");
    TestCaseDescription("This test verifies that the door lock responds correctly to lock commands");

    // Step 2: Initialize system
    TestStep(0, "Initialization", "Reset door status variables");
    doorStatus = 0;  // Reset door status

    // Step 3: Send lock command
    TestStep(1, "Send Command", "Send lock command to ECU");
    message 0x200 lockCmd;
    lockCmd.byte(0) = 0x01;  // Lock command
    output(lockCmd);

    // Step 4: Wait for response
    TestStep(1, "Wait Response", "Wait for door status response");
    long result;
    result = TestWaitForMessage(0x300, 500);  // Use message ID, timeout 500ms

    // Step 5: Verify result
    if (result == 1)
    {
        TestStepPass("Door status message received");
    }
    else
    {
        TestStepFail("Door status message not received");
    }
}

让我们逐行理解这段代码:

  • TestCaseTitle():设置测试用例的标题和编号
  • TestCaseDescription():添加测试用例的详细描述
  • TestStep():记录一个测试步骤(不判定成败)
  • TestStepPass():记录一个成功的测试步骤
  • TestStepFail():记录一个失败的测试步骤
  • TestWaitForMessage():等待指定的消息到来

步骤 5:完善 MainTest 函数

现在,我们完善 MainTest 函数:

void MainTest()
{
    // Set module title
    TestModuleTitle("Door Control Test Module");

    // Execute test cases
    TC_DoorLockTest();

    // Add more test cases as needed
}

提示:测试模块文件以 .can 为扩展名,但它的结构与仿真节点略有不同。


深入测试用例开发

在实际项目中,一个测试模块通常包含多个测试用例。让我们看一个更完整的例子。

完整的测试用例示例

variables
{
    byte doorStatus = 0;  // Store received door status
}

// Update doorStatus when message received
on message 0x300  // DoorStatus message
{
    doorStatus = this.byte(0);
}

testcase TC_DoorLockTest()
{
    // ====== Test Case Setup ======
    TestCaseTitle("TC 1.0", "Door Lock Function Test");
    TestCaseDescription("This test verifies that the door lock responds correctly to lock commands");

    // ====== Test Execution ======
    // Step 1: Initialize
    TestStep(0, "Init", "Initialize door status to unlocked");
    doorStatus = 0x00;  // Reset door status variable
    TestStepPass("Initialization complete");

    // Step 2: Send lock command
    TestStep(1, "Send Lock", "Send lock command (0x200) to door controller");
    message 0x200 lockCmd;
    lockCmd.byte(0) = 0x01;
    output(lockCmd);
    TestStepPass("Lock command sent");

    // Step 3: Wait for response
    TestStep(1, "Wait", "Wait for door status update (timeout: 500ms)");
    long result;
    result = TestWaitForMessage(0x300, 500);  // Wait for message ID 0x300

    // Step 4: Verify response
    TestStep(2, "Verify", "Check if door is now locked");
    if (result == 1)  // Message received
    {
        if (doorStatus == 0x01)  // Door is locked
        {
            TestStepPass("Door successfully locked");
        }
        else
        {
            TestStepFail("Door status incorrect: expected 0x01, got 0x%02X", doorStatus);
        }
    }
    else  // Timeout or error
    {
        TestStepFail("No response from door controller");
    }

    // Step 5: Cleanup (optional)
    TestStep(0, "Cleanup", "Reset door status");
}

关键要点

  1. 结构清晰:将测试用例分为 Setup、Execution、Cleanup 三个阶段
  2. 详细步骤:每个关键操作都用 TestStep 记录
  3. 明确判定:使用 TestStepPass 和 TestStepFail 明确标记测试结果
  4. 参数化消息:使用 TestStepFail("...", value) 可以在失败时输出具体数值

测试消息接收

TestWaitForMessage() 是测试模块中最常用的函数之一:

// Wait for a specific message by name
result = TestWaitForMessage(DoorStatus, 1000);

// Wait for a message by ID
result = TestWaitForMessage(0x201, 1000);

// Wait for any message
result = TestWaitForMessage(1000);

返回值说明

  • 1:消息成功接收
  • 0:超时
  • -2:因约束(constraint)违规而恢复
  • -1:一般错误

使用测试服务库(TSL)

测试服务库(TSL)是 CAPL 提供的强大测试工具集。它包含两类核心功能:

1. Checks(检查)

Checks 用于持续监控信号、消息或时间条件。当条件违规时,自动记录到测试报告。

示例 1:检查信号值范围

dword checkId;

// Create a check: EngineSpeed should be between 800 and 6000 RPM
checkId = ChkCreate_MsgSignalValueRangeViolation(
    EngineData,     // Message name
    EngineData::EngineSpeed,  // Signal name (use :: for signal access)
    800,            // Minimum value
    6000            // Maximum value
);

// Activate the check as a constraint
TestAddConstraint(checkId);

// ... run test ...

// Deactivate and cleanup
TestRemoveConstraint(checkId);
ChkControl_Destroy(checkId);

示例 2:检查消息周期时间

dword checkId;

// Create a check: VehicleSpeed message should occur every 100ms (±10ms)
checkId = ChkCreate_MsgAbsCycleTimeViolation(
    VehicleSpeed,   // Message name
    90,             // Minimum cycle time (ms)
    110             // Maximum cycle time (ms)
);

TestAddConstraint(checkId);

// Run test...
TestRemoveConstraint(checkId);
ChkControl_Destroy(checkId);

Checks 的工作原理

  1. 创建检查(ChkCreate_*
  2. 添加为约束(TestAddConstraint
  3. 检查在后台运行,自动监控
  4. 移除约束(TestRemoveConstraint
  5. 销毁检查(ChkControl_Destroy

2. Stimulus Generators(激励生成器)

Stimulus 用于主动生成测试数据,模拟传感器信号、环境变量变化等。

示例 1:生成斜坡信号

dword stimId;

// Create a ramp stimulus for AcceleratorPedal signal
// Start value: 0, End value: 100, Duration: 2000ms
stimId = StmCreate_Ramp(
    EngineData,                    // Message
    EngineData::AcceleratorPedal,  // Signal
    0,                             // Start value
    100,                           // End value
    5,                             // Step size
    100,                           // Cycle time (ms)
    2000,                          // Total duration (ms)
    0                              // Offset
);

// Start the stimulus
StmControl_Start(stimId);

// Wait for completion
TestWaitForTimeout(2500);

// Stop and cleanup
StmControl_Stop(stimId);
ChkControl_Destroy(stimId);

示例 2:切换信号值

dword stimId;

// Create a toggle stimulus for WindowSwitch signal
stimId = StmCreate_Toggle(
    DoorControl,                  // Message
    DoorControl::WindowSwitch,    // Signal
    0,                            // Value 1 (window up)
    1,                            // Value 2 (window down)
    500,                          // Toggle interval (ms)
    3                             // Number of toggles
);

StmControl_Start(stimId);
TestWaitForTimeout(2000);
StmControl_Stop(stimId);
ChkControl_Destroy(stimId);

实际案例:完整的测试用例

让我们用一个完整的例子,测试车门升降功能,同时使用 Checks 和 Stimulus:

testcase TC_WindowControlTest()
{
    dword checkId, stimId;

    // ====== Setup ======
    TestCaseTitle("TC 2.0", "Window Control Test");
    TestCaseDescription("Test window up/down movement with stimulus and checks");

    // Create check: Verify window position signal is valid
    checkId = ChkCreate_MsgSignalValueRangeViolation(
        DoorFeedback,
        DoorFeedback::WindowPosition,
        0,    // Min: fully up
        100   // Max: fully down
    );
    TestAddConstraint(checkId);

    // ====== Test Execution ======
    // Step 1: Move window down
    TestStep(1, "Stimulus", "Generate window down signal");
    stimId = StmCreate_Toggle(
        DoorControl,
        DoorControl::WindowSwitch,
        0, 1, 500, 1  // Toggle once: 0->1
    );
    StmControl_Start(stimId);
    TestStepPass("Window down signal generated");

    // Step 2: Wait for movement
    TestStep(1, "Wait", "Wait for window to move down (2000ms)");
    TestWaitForTimeout(2000);
    StmControl_Stop(stimId);
    ChkControl_Destroy(stimId);

    // Step 3: Verify position
    TestStep(2, "Verify", "Check if window moved to down position");
    if (windowPosition >= 50)  // Threshold for "down"
    {
        TestStepPass("Window moved down successfully");
    }
    else
    {
        TestStepFail("Window did not move: position is %d", windowPosition);
    }

    // ====== Cleanup ======
    TestRemoveConstraint(checkId);
    ChkControl_Destroy(checkId);
}

运行和调试测试模块

在 CANoe 中添加测试模块

  1. 打开 CANoe 工程
  2. 在菜单栏选择 TestTest Setup
  3. 右键点击 Test Environment,选择 Insert Test Module
  4. 浏览并选择你的测试模块文件(.can
  5. 点击 OK

[!SCREENSHOT]
位置:CANoe Test Setup 窗口
内容:展示新添加的测试模块
标注:圈出测试模块节点和 Test Units 面板

运行测试

  1. Test Setup 窗口中,右键点击测试模块
  2. 选择 StartStart with Report
  3. 观察 Test Report 窗口,查看测试进度和结果

[!SCREENSHOT]
位置:CANoe Test Report 窗口
内容:显示测试执行过程和结果
标注:圈出 Passed/Failed 状态和详细步骤

查看测试报告

测试报告会显示:

  • 测试用例列表:所有执行的 testcase
  • 测试步骤详情:每个 TestStep 的时间和结果
  • 判定结果:Pass/Fail/Inconclusive
  • 时间戳:每个步骤的执行时间

最佳实践

1. 测试用例设计原则

单一职责:每个 testcase 只测试一个功能点。

// Good: One functionality per test case 
testcase TC_DoorLockTest() { ... }
testcase TC_WindowUpTest() { ... }

// Avoid: Mixing multiple functionalities
testcase TC_DoorAndWindowTest() { ... }  // Don't do this

明确命名:使用描述性的测试用例名称。

// Good: Clear and descriptive
testcase TC_LockCommandResponseTime() { ... }

// Avoid: Vague names
testcase TC_Test1() { ... }

2. 提高报告可读性

详细描述:为每个 testcase 添加清晰的描述。

TestCaseTitle("TC 3.0", "Door Lock Response Time");
TestCaseDescription(
    "This test verifies that the door controller responds to lock commands "
    "within 100ms. The test sends a lock command and measures the time until "
    "the door status feedback is received."
);

分级步骤:使用 LevelOfDetail 参数区分重要步骤。

TestStep(0, "Critical", "System initialization");  // Very important
TestStep(1, "Normal", "Send test command");        // Standard step
TestStep(2, "Detail", "Parse response data");      // Detailed info

3. 模块化和重用

创建测试库:将常用的检查和激励封装成函数。

// Test helper function
void WaitForDoorLock()
{
    long result;
    result = TestWaitForMessage(DoorStatus, 1000);
    if (result != 1)
    {
        TestStepFail("Door status not received");
    }
}

// Reuse in multiple test cases
testcase TC_LockTest1()
{
    // ... setup ...
    WaitForDoorLock();
    // ... verify ...
}

本篇总结

通过本篇的学习,我们掌握了:

  • 测试模块的概念:与仿真节点不同,测试模块专注于验证 ECU 的行为
  • 核心组件
    • testcase:测试的基本单元
    • MainTest:测试模块的入口和调度器
    • TestStep:记录和判定测试步骤
  • 测试服务库(TSL)
    • Checks:持续监控信号、消息、时间等条件
    • Stimulus:主动生成测试数据,模拟信号变化
  • 测试报告:自动生成详细的测试执行记录

测试模块是 CAPL 开发流程中的重要一环,它让你能够自动化验证 ECU 的功能,提高测试效率和可靠性。

课后练习

  1. 基础练习:修改本篇的 TC_DoorLockTest,添加一个测试"解锁命令"的测试用例。

  2. 进阶练习:使用 ChkCreate_MsgSignalValueRangeViolation 检查 EngineSpeed 信号是否在合理范围内(0-8000 RPM)。

  3. 挑战练习:创建一个测试用例,使用 StmCreate_Ramp 生成加速踏板信号,从 0% 逐渐增加到 100%,并检查发动机转速的响应。