使用规则化测试(Rule-Based Testing)为 F Prime 组件编写单元测试
使用规则化测试Rule-Based Testing为 F Prime 组件编写单元测试【免费下载链接】fprimeF´ - A flight software and embedded systems framework项目地址: https://gitcode.com/GitHub_Trending/fpr/fprime规则化测试Rule-Based TestingRBT是 F PrimeF´飞行软件与嵌入式系统框架提供的一种单元测试方法论测试不再是手写的一长串固定断言而是由规则Rule与场景Scenario两种积木以不同方式装配而成。本文将基于官方 how-to 指南以Svc/Ccsds/ApidManager组件为完整实战案例带你从零掌握 RBT 的四大构建块、七步编写流程、随机化测试策略与规则参数化等高级技巧。读完本文你将能够为自己的有状态 F´ 组件搭建一套影子状态 规则 场景的自动化测试体系用少量代码获得远超手写测试的状态空间覆盖。一、什么是规则化测试方法论核心规则化测试是一种单元测试方法论其核心思想是测试由一组构建块规则以多种不同方式场景装配而成。规则描述测什么、何时测场景决定按什么顺序、应用多少次。每一条规则都建模一组行为包含两个要素前置条件precondition说明该规则在什么情况下可以被应用——它是一个只读的谓词函数返回布尔值动作action执行测试——驱动被测系统并断言结果符合预期。规则随后被装配成不同的序列来构成测试序列可以是随机生成的且数量可以非常庞大。这种机制带来了两个直接收益覆盖广度随机场景能在状态空间中自动探索大量组合路径置信度通过影子状态shadow state在每次动作后与被测组件同步校验对组件行为给出高置信度的验证。F´ 中的规则化测试框架由 STest 模块 提供。STest 是专门为规则 场景式单元测试设计的框架规则是参数化于状态类型之上的模板类场景则定义了装配与运行规则的各种策略。从 STest/STest/Rule/Rule.hpp 的源码可以看到Rule模板类对外暴露了apply(State)方法其实现是先断言前置条件成立、再执行动作void apply(State state) { ASSERT_TRUE(this-precondition(state)) precondition failed applying rule this-m_name; this-action(state); }前置条件开始之前你应当具备F Prime 单元测试经验可参考官方的 LedBlinker 教程逐步入门一个已生成的 UT 构建fprime-util generate --ut。二、何时使用规则化测试RBT 并非万金油选择前先评估被测组件的特点场景推荐组件定义了状态机或内部状态状态会影响行为✅ 使用 RBT希望用规则化测试获得宽覆盖的测试集✅ 使用 RBT组件行为纯函数式无状态传统 UT 即可轻松覆盖❌ 继续使用传统测试判断标准很简单有状态、需要探索状态空间就用 RBT纯函数、无状态传统 UT 更直接高效。三、测试结构总览四大构建块一个规则化测试包含四种主要构建块其中两种是 RBT 特有的1. 影子状态类Shadow State Class位于test/ut/TestState/测试侧的模型镜像组件的内部状态。前置条件查询它来决定规则是否可应用动作在驱动组件的同时与组件保持步调一致地更新它。2. 规则实现Rule Implementations位于test/ut/Rules/每条规则是一个STest::Rule的 C 结构体包含两个方法precondition()返回true时该规则可被应用action()驱动组件并断言测试结果。其余两种构建块是所有 F´ 单元测试共有的3. 测试器类Tester Classtest/ut/MyComponentTester.hpp继承 GTest 基类如MyComponentGTestBase。在 RBT 场景下它额外包含一个类型为TestState的shadow状态成员并声明规则。4. 测试入口Test Maintest/ut/MyComponentTestMain.cpp实例化规则并通过场景应用它们定向测试按手工指定的顺序应用规则随机化测试则按随机顺序应用规则、迭代很多次。四、实战示例组件ApidManager本文的实战案例是Svc/Ccsds/ApidManager组件——一个将ComQueue的输出映射到 CCSDS APID 的被动组件passive component。它的本质是一张查找表把标识符APID映射到序号sequence count并跟踪每个 APID 的下一个序列计数。它暴露两个端口getApidSeqCountIn为给定 APID 返回下一个序列计数validateApidSeqCountIn校验输入的序列计数是否等于给定 APID 的下一个序列计数。简单说一个端口发放序列计数另一个端口校验进来的序列计数。其 FPP 模型见 Svc/Ccsds/ApidManager/ApidManager.fppmodule Svc { module Ccsds { Maps output of ComQueue to CCSDS APIDs passive component ApidManager { Port to validate a given sequence count for a given APID guarded input port validateApidSeqCountIn: Ccsds.ApidSequenceCount Port to request a sequence count for a given APID guarded input port getApidSeqCountIn: Ccsds.ApidSequenceCount Deframing received an unexpected sequence count event UnexpectedSequenceCount(transmitted: U16, expected: U16) \ severity warning low \ format Unexpected sequence count received. Packets may have been dropped. Transmitted: {} | Expected on board: {} # ... 标准 AC 端口timeCaller、logTextOut、logOut... } } }该组件的实际测试代码位于 Svc/Ccsds/ApidManager/test/ut/目录结构为test/ut/ ├── ApidManagerTestMain.cpp # 测试入口定向 随机场景 ├── ApidManagerTester.cpp/.hpp # 测试器类 ├── Rules/ │ ├── GetSeqCount.cpp # GetSeqCount 规则组 │ └── ValidateSeqCount.cpp # ValidateSeqCount 规则组 └── TestState/ ├── TestState.cpp # 影子状态实现 └── TestState.hpp # 影子状态定义五、七步编写流程Step 1识别需要覆盖的行为列出组件可能表现出的不同行为。对于ApidManager待测行为采取的动作预期结果获取已跟踪 APID 的计数用已跟踪 APID 调用getApidSeqCountIn返回下一个计数无事件获取新 APID 的计数用未跟踪 APID 调用getApidSeqCountIn返回 0注册该 APID无事件校验正确计数用期望计数调用validateApidSeqCountIn无事件校验错误计数用意外计数调用validateApidSeqCountIn发送UnexpectedSequenceCount事件表格的每一行对应一条规则。组件内部状态越简单规则与行为的对应就越直接。对于定义状态机的组件通常每个状态迁移至少对应一条规则前置条件自然检查状态机的某个特定状态。Step 2添加测试状态与规则目录在组件目录下运行fprime-util new --rule-based-test该命令会搭建test/ut/Rules/和test/ut/TestState/目录骨架当然你也可以手工创建。Step 3定义影子测试状态影子测试状态是测试侧的结构用于镜像组件的内部状态。其目的是在测试期间跟踪组件的期望状态使规则能基于期望状态进行断言同时驱动规则的前置条件。影子状态的设计原则只镜像需要的内容不要复制整个组件实现只保留前置条件和断言所需的状态保持同步动作在驱动组件的同时按预期行为同步更新影子必要时提供辅助方法影子TestState是普通 C 类可以自由提供辅助方法。在test/ut/TestState/TestState.hpp中定义影子状态。对ApidManager我们用std::map测试代码中允许使用镜像 APID 到序列计数的映射并提供镜像组件行为的辅助方法。仓库中的真实实现TestState.hpp如下class ApidManagerTestState { public: //! 镜像组件内部的 APID-序列计数映射对应组件 m_apidSequences std::mapComCfg::Apid::T, U16 shadow_seqCounts; //! 当 shadow_seqCounts 达到 MAX_TRACKED_APIDS 条时为 true bool shadow_isTableFull false; public: //! 返回 apid 的当前期望计数并把影子推进到下一个值 U16 shadow_getAndIncrementSeqCount(ComCfg::Apid::T apid); //! 模拟以 expectedSeqCount 调用 validateApidSeqCountIn 后的影子推进 void shadow_validateApidSeqCount(ComCfg::Apid::T apid, U16 expectedSeqCount); //! 从影子当前跟踪的 APID 集合中均匀随机返回一个 ComCfg::Apid::T shadow_getRandomTrackedApid() const; };这些方法在TestState.cpp中实现。以 TestState.cpp 中的shadow_getAndIncrementSeqCount为例可以看到影子与组件行为精确对齐的细节——包括 14 位序列计数器的回绕逻辑1 SpacePacketSubfields::SeqCountWidthSeqCountWidth定义于 Svc/Ccsds/Types/FppConstantsAc.hppU16 ApidManagerTestState::shadow_getAndIncrementSeqCount(ComCfg::Apid::T apid) { auto it this-shadow_seqCounts.find(apid); if (it ! this-shadow_seqCounts.end()) { // APID 已被跟踪返回当前计数并推进到下一个 U16 current it-second; it-second static_castU16((current 1) % (1 SpacePacketSubfields::SeqCountWidth)); return current; } // APID 尚未跟踪注册它从计数 0 开始 this-shadow_seqCounts[apid] static_castU16(1); // 下一个期望值是 1 return 0; // 第一次返回的计数是 0 }而shadow_getRandomTrackedApid()使用了 STest 的随机工具STest::Random::lowerUpper并在影子为空时通过FW_ASSERT快速失败——这保证了随机规则永远只挑选已被跟踪的 APIDComCfg::Apid::T ApidManagerTestState::shadow_getRandomTrackedApid() const { FW_ASSERT(!this-shadow_seqCounts.empty()); U32 idx STest::Random::lowerUpper(0, static_castU32(this-shadow_seqCounts.size()) - 1); return std::next(this-shadow_seqCounts.begin(), idx)-first; }Step 4在 ComponentTester 类中声明规则与影子状态成员添加影子状态成员在MyComponent/test/ut/MyComponentTester.hpp的 ComponentTester 类中声明影子状态成员。对ApidManager真实代码ApidManagerTester.hpp为#include Svc/Ccsds/ApidManager/test/ut/TestState/TestState.hpp class ApidManagerTester : public ApidManagerGTestBase { // ... 其他代码 ... public: ApidManager component; ApidManagerTestState shadow; };这样测试器同时持有被测组件的两份并行模型实际组件实例component与影子状态shadow。声明规则规则使用辅助宏FW_RBT_DEFINE_RULE(TesterClass, GroupName, RuleName)声明。该宏会创建一个返回bool的GroupName__RuleName__precondition()方法一个驱动测试的GroupName__RuleName__action()方法一个类型为STest::Rule的GroupName__RuleNameC 结构体可在测试用例中实例化。在 ComponentTester 头文件中包含TestUtils/RuleBasedTesting.hpp然后按 Step 1 识别出的每个行为声明一条FW_RBT_DEFINE_RULE。ApidManager 的真实声明为#include TestUtils/RuleBasedTesting.hpp class ApidManagerTester : public ApidManagerGTestBase { // ... 其他代码 ... public: FW_RBT_DEFINE_RULE(ApidManagerTester, GetSeqCount, Existing); FW_RBT_DEFINE_RULE(ApidManagerTester, GetSeqCount, NewOk); FW_RBT_DEFINE_RULE(ApidManagerTester, ValidateSeqCount, Ok); FW_RBT_DEFINE_RULE(ApidManagerTester, ValidateSeqCount, Failure); };以上定义了 4 条规则两条属于GetSeqCount组两条属于ValidateSeqCount组。建议将规则划分到有逻辑意义的组中——这里的分组依据是它们驱动的输入端口。Step 5实现规则为每个规则组创建一个test/ut/Rules/GroupName.cpp文件。每条规则必须实现两个方法前置条件bool GroupName__RuleName__precondition()返回true时该规则可被应用动作void GroupName__RuleName__action()驱动组件并验证行为。仓库中Rules/GetSeqCount.cpp源码的真实实现// 当至少有一个 APID 已被跟踪时该规则可应用 bool ApidManagerTester::GetSeqCount__Existing__precondition() const { return !this-shadow.shadow_seqCounts.empty(); } void ApidManagerTester::GetSeqCount__Existing__action() { this-clearHistory(); // 用影子辅助方法随机取一个已被跟踪的 APID ComCfg::Apid::T apid this-shadow.shadow_getRandomTrackedApid(); // 调用组件端口并同步在影子状态中镜像行为 U16 returned this-invoke_to_getApidSeqCountIn(0, apid, 0); U16 expected this-shadow.shadow_getAndIncrementSeqCount(apid); // 断言结果及其他属性如事件是否符合预期 ASSERT_EQ(returned, expected) Sequence count mismatch for APID static_castU16(apid); ASSERT_EVENTS_SIZE(0); }GetSeqCount.NewOk规则则演示了如何用影子状态驱动前置条件的翻转——当影子表已满时置位shadow_isTableFull使NewOk规则的前置条件在后续迭代中变为false从而阻止继续注册新 APID保护组件内部MAX_TRACKED_APIDS上限bool ApidManagerTester::GetSeqCount__NewOk__precondition() const { return !this-shadow.shadow_isTableFull; } void ApidManagerTester::GetSeqCount__NewOk__action() { this-clearHistory(); constexpr U16 maxTrackedApids ApidManager::MAX_TRACKED_APIDS; if (this-shadow.shadow_seqCounts.size() maxTrackedApids) { // 表已满更新影子标志使前置条件翻转 this-shadow.shadow_isTableFull true; return; } Fw::OptionalComCfg::Apid::T apidOption CcsdsTestUtils::getRandomApid(); if (!apidOption.has_value()) { GTEST_SKIP() Could not find a valid apid\n; } const auto apid apidOption.value(); U16 returned this-invoke_to_getApidSeqCountIn(0, apid, 0); U16 expected this-shadow.shadow_getAndIncrementSeqCount(apid); ASSERT_EQ(returned, expected) First sequence count for new APID ...; ASSERT_EVENTS_SIZE(0); }Rules/ValidateSeqCount.cpp源码展示了事件断言的用法——ValidateSeqCount.Failure在传入错误计数后断言恰好产生 1 条UnexpectedSequenceCount事件且事件字段值正确bool ApidManagerTester::ValidateSeqCount__Failure__precondition() const { return !this-shadow.shadow_seqCounts.empty(); } void ApidManagerTester::ValidateSeqCount__Failure__action() { this-clearHistory(); ComCfg::Apid::T apid this-shadow.shadow_getRandomTrackedApid(); U16 correctCount this-shadow.shadow_seqCounts.at(apid); // 加 1对 14 位计数器取模构造一个可证明的错误计数 U16 wrongCount static_castU16((correctCount 1) % (1 SpacePacketSubfields::SeqCountWidth)); this-invoke_to_validateApidSeqCountIn(0, apid, wrongCount); this-shadow.shadow_validateApidSeqCount(apid, wrongCount); ASSERT_EVENTS_UnexpectedSequenceCount_SIZE(1); ASSERT_EVENTS_UnexpectedSequenceCount(0, wrongCount, correctCount); }Step 6编写测试入口规则化测试通常包含两类测试用例定向测试Targeted按固定顺序应用规则用于验证特定路径——适合确认已知行为、尽早捕获回归随机化测试Randomized按随机顺序应用规则并迭代很多次用于探索状态空间、锤击边界条件与意外交互。创建test/ut/MyComponentTestMain.cpp。仓库中真实的 ApidManagerTestMain.cpp 如下#include STest/Random/Random.hpp #include STest/Scenario/BoundedScenario.hpp #include STest/Scenario/RandomScenario.hpp #include Svc/Ccsds/ApidManager/test/ut/ApidManagerTester.hpp // 定向测试手工指定的序列验证已知行为 TEST(ApidManager, GetSequenceCounts) { ApidManagerTester tester; ApidManagerTester::GetSeqCount__NewOk ruleNewOk; ApidManagerTester::GetSeqCount__Existing ruleExisting; ruleNewOk.apply(tester); // 注册新 APID期望计数 0 ruleExisting.apply(tester); // 同一 APID 再次获取期望计数 1 } // 定向测试校验正确/错误计数的事件行为 TEST(ApidManager, ValidateSequenceCounts) { ApidManagerTester tester; ApidManagerTester::GetSeqCount__NewOk ruleNewOk; ApidManagerTester::ValidateSeqCount__Ok ruleValidateOk; ApidManagerTester::ValidateSeqCount__Failure ruleValidateFailure; ruleNewOk.apply(tester); // 注册 APID 使校验规则可触发 ruleValidateOk.apply(tester); // 校验正确计数无事件 ruleValidateFailure.apply(tester); // 校验错误计数产生事件 } // 随机化测试随机顺序应用规则迭代 10,000 次 TEST(ApidManager, RandomizedTesting) { U32 numRulesToApply 10000; ApidManagerTester tester; ApidManagerTester::GetSeqCount__Existing ruleGetExisting; ApidManagerTester::GetSeqCount__NewOk ruleGetNewOk; ApidManagerTester::ValidateSeqCount__Ok ruleValidateOk; ApidManagerTester::ValidateSeqCount__Failure ruleValidateFailure; STest::RuleApidManagerTester* rules[] { ruleGetExisting, ruleGetNewOk, ruleValidateOk, ruleValidateFailure, }; // 在随机序列中应用指定规则共 10,000 次迭代 STest::RandomScenarioApidManagerTester random(Random Rules, rules, FW_NUM_ARRAY_ELEMENTS(rules)); STest::BoundedScenarioApidManagerTester bounded(Bounded Random Rules Scenario, random, numRulesToApply); const U32 numSteps bounded.run(tester); printf(Ran %u steps.\n, numSteps); } int main(int argc, char** argv) { STest::Random::seed(); ::testing::InitGoogleTest(argc, argv); return RUN_ALL_TESTS(); }场景Scenario控制规则如何被应用STest 提供了丰富的场景类型全部位于 STest/STest/Scenario/场景类型行为对应头文件RandomScenario每一步从可应用的规则中随机挑选一条RandomScenario.hppBoundedScenario包装另一个场景运行 N 步后停止BoundedScenario.hppSequenceScenario按固定顺序应用规则SequenceScenario.hppRuleSequenceScenario以固定或随机顺序应用一组规则RuleSequenceScenario.hppRepeatedRuleScenario在场景内重复应用某条规则RepeatedRuleScenario.hppConditionalScenario当某条件成立时运行场景ConditionalScenario.hppSelectedScenario随机挑选一个子场景运行SelectedScenario.hppInterleavedScenario随机交错运行一组场景InterleavedScenario.hppIteratedScenario迭代运行一组场景IteratedScenario.hppConditionalIteratedScenario迭代运行某场景直到条件成立ConditionalIteratedScenario.hppBoundedIteratedScenario以固定迭代次数上界运行BoundedIteratedScenario.hppRandomlyBoundedScenario以上界随机选择的迭代运行RandomlyBoundedScenario.hpp正如 STest/README.md 所述迭代型与随机型场景允许你用相对简单的规格构造探索大量行为的复杂测试这些测试通常不可能手工编写例如应用数千乃至数百万条规则。F´ 目前使用 STest 进行组件单元测试主要场景模式有两种短规则序列先搭建系统状态再测试特定规则和随机场景自动生成更复杂的测试。Step 7在 CMake 中注册所有 UT 源码将测试器、影子状态及所有规则文件加入组件CMakeLists.txt的register_fprime_utregister_fprime_ut( SOURCES ${CMAKE_CURRENT_LIST_DIR}/test/ut/ApidManagerTestMain.cpp ${CMAKE_CURRENT_LIST_DIR}/test/ut/ApidManagerTester.cpp ${CMAKE_CURRENT_LIST_DIR}/test/ut/TestState/TestState.cpp ${CMAKE_CURRENT_LIST_DIR}/test/ut/Rules/GetSeqCount.cpp ${CMAKE_CURRENT_LIST_DIR}/test/ut/Rules/ValidateSeqCount.cpp AUTOCODER_INPUTS ${CMAKE_CURRENT_LIST_DIR}/ApidManager.fpp DEPENDS Svc_Ccsds_Types STest UT_AUTO_HELPERS )注意DEPENDS中必须声明STest依赖——因为规则与场景都来自 STest 框架。如何新增一条规则速查总结在测试器头文件test/ut/MyComponentTester.hpp中添加新的FW_RBT_DEFINE_RULE声明FW_RBT_DEFINE_RULE(ComponentNameTester, GroupName, RuleName);在对应的test/ut/Rules/GroupName.cpp中实现前置条件与动作方法。若是全新的规则组则新建GroupName.cpp文件 bool ComponentNameTester::GroupName__RuleName__precondition() const { return true; // TODO: 可选的前置条件逻辑恒为 true 也是一种选择 } void ComponentNameTester::GroupName__RuleName__action() { // TODO: 实现规则行为 }别忘了把新文件加入 CMakeLists.txt 的register_fprime_ut随后即可在测试入口中通过场景应用该规则。提示要添加动作与前条件样板代码最简单的方法是复制粘贴一条已有规则实现再把 RuleName 换成新规则名。六、最佳实践保持前置条件无副作用side-effect free前置条件只读影子状态绝不修改任何状态保证场景调度可重复、可预测影子状态保持最小且显式只镜像前置条件和断言真正会用到的状态避免过度复制组件实现每个动作开头调用this-clearHistory()清空历史记录确保断言只针对本条规则产生的行为而非之前规则的遗留每条规则对应一个独立的行为属性而不是一个端口一条规则规则粒度以行为为单位便于随机场景灵活组合定向序列测试已知行为随机序列锤击边界定向测试非随机用于验证预期行为随机化序列在挖掘边界情况和意外交互方面非常强大。七、高级用法理解 FW_RBT_DEFINE_RULE 宏FW_RBT_DEFINE_RULE(TesterClass, GroupName, RuleName)在测试器类体内展开为三样东西宏定义见 TestUtils/RuleBasedTesting.hpp一个bool GroupName__RuleName__precondition() const方法声明——在.cpp文件中实现一个void GroupName__RuleName__action()方法声明——在.cpp文件中实现一个struct GroupName__RuleName : public STest::RuleTesterClass规则定义其precondition()与action()分别委托给上面 1、2 两个方法。关键设计在于前置条件与动作在测试器上实现而不是直接在规则结构体中实现。这使 F Prime 测试断言宏如ASSERT_EVENTS_*、ASSERT_TLM_*能在规则体内正常工作——因为这些宏展开为this-...而this必须是测试器实例。规则在构造时的参数化如上所述FW_RBT_DEFINE_RULE宏的空构造函数使得规则无法在实例化时传参。某些场景下参数化很有用这时可以内联规则结构体并指定构造函数class ApidManagerTester : public ApidManagerGTestBase { // ... 其他代码 ... public: // -------------------------------------------- // 参数化规则GetSeqCount__Repeated // -------------------------------------------- struct GetSeqCount__Repeated : public STest::RuleApidManagerTester { // 规则的成员变量 U32 m_iterations; // 构造函数 explicit GetSeqCount__Repeated(U32 iterations) : STest::RuleApidManagerTester(GetSeqCount__Repeated), m_iterations(iterations) {} bool precondition(const ApidManagerTester tester) override { return !tester.shadow.shadow_seqCounts.empty(); } void action(ApidManagerTester tester) override { // 注意在手工编写的规则体内F´ 宏ASSERT_EVENT_*、ASSERT_TLM_* 等 // 不可用因为 this 指向的是规则而非测试器 tester.clearHistory(); for (U32 i 0; i this-m_iterations; i) { ComCfg::Apid::T apid tester.shadow.shadow_getRandomTrackedApid(); U16 returned tester.invoke_to_getApidSeqCountIn(0, apid, 0); U16 expected tester.shadow.shadow_getAndIncrementSeqCount(apid); ASSERT_EQ(returned, expected); } } }; };随后在测试入口中以期望值实例化ApidManagerTester::GetSeqCount__Repeated rule5(5); // 运行 5 次迭代 ApidManagerTester::GetSeqCount__Repeated rule25(25); // 运行 25 次迭代 rule5.apply(tester); rule25.apply(tester);重要F Prime 测试断言宏ASSERT_EVENTS_*等在手工编写的规则结构体中不可用因为此时this指向规则而不是测试器。应通过action传入的tester引用来调用断言如tester.assertEvents_...()或者像FW_RBT_DEFINE_RULE宏那样把规则的precondition()/action()内联委托给测试器上的方法。八、参考资料Svc/Ccsds/ApidManager/test/ut/本文实战案例的完整测试代码Svc/Ccsds/ApidManager/ApidManager.fpp被测组件的 FPP 模型TestUtils/RuleBasedTesting.hppFW_RBT_DEFINE_RULE宏定义STest/README.mdSTest 框架状态、规则、场景概念说明STest/STest/Rule/Rule.hppRule模板类接口STest/STest/Scenario/全部场景类型实现掌握规则化测试后你便拥有了一套可复用的方法论影子状态精确镜像组件状态规则以行为为粒度封装前置条件与动作场景以固定或随机序列驱动规则运行——三者结合让有状态组件的单元测试从手工枚举用例升级为自动探索状态空间以可维护的测试代码换取更高置信度。【免费下载链接】fprimeF´ - A flight software and embedded systems framework项目地址: https://gitcode.com/GitHub_Trending/fpr/fprime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考