CAPL Script

Test Module Development

Learning Objectives

After completing this article, you will be able to:

  • Understand the basic concepts and architecture of CAPL Test Modules
  • Master the core components of test modules: testcase, MainTest, and Test Steps
  • Learn to use the Test Service Library (TSL) for Checks and Stimulus functions
  • Independently create a complete test module and generate test reports
  • Have a clear understanding of the final project in this tutorial series

From Simulation to Testing

In the previous articles, we've learned how to use CAPL to create simulation nodes. Simulation nodes can simulate real ECUs, send messages, and respond to events, just like actual hardware.

However, as an automotive electronics engineer, you might ask:

"My ECU code is finished, and the simulation runs smoothly. But how do I know it really works according to requirements? Are there any bugs?"

At this point, you need to transition from simulation to testing. CAPL provides specialized test module functionality that allows you to write automated test cases to verify whether ECU behavior meets expectations.

In simple terms:

  • Simulation nodes = "Pretend" other ECUs exist
  • Test modules = "Verify" my ECU works correctly

What is a Test Module?

A Test Module is a special program type in CAPL used for automated testing. Unlike simulation nodes, test modules don't participate in bus communication. Instead, they:

  1. Send Stimulus: Send test commands to the ECU under test
  2. Check Responses: Verify whether ECU behavior meets expectations
  3. Generate Reports: Automatically record test results

Core Components of Test Modules

A complete test module contains the following parts:

1. testcase (Test Case)

testcase is the basic unit of testing, defining a complete test scenario:

testcase TC_DoorLockTest()
{
    // Test implementation
}

2. MainTest (Main Test Function)

MainTest is the entry point of the test module, responsible for scheduling and organizing all test cases:

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

3. Test Steps (Test Step)

Test steps are the smallest units within test cases, used to record and judge the testing process:

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

4. Test Service Library (TSL)

TSL provides rich testing tool functions, including:

  • Checks: Verify signals, messages, timing, etc.
  • Stimulus: Generate test data, simulate signal changes

Creating Your First Test Module

Let's start with a simple example to test the door lock functionality.

Step 1: Create a New Test Module File

  1. In CAPL Browser, click FileNewCAPL Test Module File
  2. The system will create a test module file with basic structure

Step 2: Write the MainTest Function

First, we create an empty MainTest function framework:

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

    // Step 2: Add more test cases here
}

Step 3: Create the First Test Case

Now, we add a test case:

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

Step 4: Add Test Steps

Next, we complete this test case:

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");
    }
}

Let's understand this code line by line:

  • TestCaseTitle(): Set the test case title and number
  • TestCaseDescription(): Add detailed description of the test case
  • TestStep(): Record a test step (without judgment of success/failure)
  • TestStepPass(): Record a successful test step
  • TestStepFail(): Record a failed test step
  • TestWaitForMessage(): Wait for a specified message to arrive

Step 5: Complete the MainTest Function

Now, we complete the MainTest function:

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

    // Execute test cases
    TC_DoorLockTest();

    // Add more test cases as needed
}

Note: Test module files have a .can extension, but their structure is slightly different from simulation nodes.


Deep Dive into Test Case Development

In real projects, a test module usually contains multiple test cases. Let's look at a more complete example.

Complete Test Case Example

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");
}

Key Points:

  1. Clear Structure: Divide the test case into Setup, Execution, and Cleanup phases
  2. Detailed Steps: Record each key operation with TestStep
  3. Clear Judgments: Use TestStepPass and TestStepFail to clearly mark test results
  4. Parameterized Messages: Using TestStepFail("...", value) can output specific values when failing

Testing Message Reception

TestWaitForMessage() is one of the most commonly used functions in test modules:

// 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);

Return Value Explanation:

  • 1: Message successfully received
  • 0: Timeout
  • -2: Aborted due to constraint violation
  • -1: General error

Using the Test Service Library (TSL)

The Test Service Library (TSL) is a powerful testing toolkit provided by CAPL. It contains two core types of functions:

1. Checks

Checks are used to continuously monitor signals, messages, or time conditions. When conditions are violated, they are automatically recorded in the test report.

Example 1: Check Signal Value Range

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);

Example 2: Check Message Cycle Time

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);

How Checks Work:

  1. Create the check (ChkCreate_*)
  2. Add as constraint (TestAddConstraint)
  3. Check runs in the background, automatically monitoring
  4. Remove constraint (TestRemoveConstraint)
  5. Destroy check (ChkControl_Destroy)

2. Stimulus Generators

Stimulus is used to actively generate test data, simulating sensor signals, environmental variable changes, etc.

Example 1: Generate Ramp Signal

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);

Example 2: Toggle Signal Values

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);

Real-World Case: Complete Test Case

Let's use a complete example to test the door window functionality, using both Checks and 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);
}

Running and Debugging Test Modules

Adding Test Modules in CANoe

  1. Open a CANoe project
  2. In the menu bar, select TestTest Setup
  3. Right-click on Test Environment, select Insert Test Module
  4. Browse and select your test module file (.can)
  5. Click OK

[!SCREENSHOT]
Location: CANoe Test Setup window
Content: Shows the newly added test module
Annotation: Circle the test module node and Test Units panel

Running Tests

  1. In the Test Setup window, right-click on the test module
  2. Select Start or Start with Report
  3. Observe the Test Report window to see test progress and results

[!SCREENSHOT]
Location: CANoe Test Report window
Content: Shows test execution process and results
Annotation: Circle Passed/Failed status and detailed steps

Viewing Test Reports

Test reports display:

  • Test Case List: All executed testcases
  • Test Step Details: Time and results for each TestStep
  • Judgment Results: Pass/Fail/Inconclusive
  • Timestamps: Execution time for each step

Best Practices

1. Test Case Design Principles

Single Responsibility: Each testcase should test only one functionality.

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

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

Clear Naming: Use descriptive test case names.

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

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

2. Improving Report Readability

Detailed Descriptions: Add clear descriptions for each 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."
);

Hierarchical Steps: Use the LevelOfDetail parameter to distinguish important steps.

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

3. Modularity and Reuse

Create Test Libraries: Encapsulate common checks and stimulus into functions.

// 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 ...
}

Summary

Through this article, we've mastered:

  • Test Module Concepts: Unlike simulation nodes, test modules focus on verifying ECU behavior
  • Core Components:
    • testcase: The basic unit of testing
    • MainTest: Entry point and scheduler of test modules
    • TestStep: Recording and judging test steps
  • Test Service Library (TSL):
    • Checks: Continuously monitor signals, messages, timing, and other conditions
    • Stimulus: Actively generate test data, simulate signal changes
  • Test Reports: Automatically generate detailed test execution records

Test modules are an important part of the CAPL development process. They enable you to automatically verify ECU functionality, improving testing efficiency and reliability.

Exercises

  1. Basic Exercise: Modify the TC_DoorLockTest from this article, adding a test case for the "unlock command".

  2. Advanced Exercise: Use ChkCreate_MsgSignalValueRangeViolation to check whether the EngineSpeed signal is within a reasonable range (0-8000 RPM).

  3. Challenge Exercise: Create a test case that uses StmCreate_Ramp to generate an accelerator pedal signal, gradually increasing from 0% to 100%, and check the engine speed response.