☰
F´ 中的规则与场景驱动测试:基于 STest 的组件单元测试框架详解
2026/9/25 3:05:33 网站建设 项目流程
  • 嵌入式
  • 系统编程

【免费下载链接】fprime

F´ - A flight software and embedded systems framework

项目地址:https://gitcode.com/gh_mirrors/fpri/fprime
点击查看免费下载

导读

STest 是 F´(F Prime)飞行软件与嵌入式系统框架中内置的一个单元测试框架,其核心思想是用规则(Rule)与场景(Scenario)来构造并运行单元测试。本文以 STest/README.md 为骨架,深入讲解 State、Rule、Scenario 三大核心概念,并结合仓库中的源码实现(如 Rule.hpp、Scenario.hpp)与 F Prime 组件测试的真实用例(如 Svc/GroundInterface 的测试),说明如何用这种结构化的、可自动生成测试的方式,替代手工编写海量用例,提升组件测试的覆盖率与可维护性。

为什么需要规则与场景:单元测试的结构化革命

在传统的单元测试中,每个测试用例通常是一个独立的函数:设置状态、执行被测逻辑、断言结果。当被测组件状态众多、行为交错时,手工编写并维护这些用例会变得繁琐且容易遗漏边界。

STest 的答案是把测试从"用例集合"提升为"规则系统"。它由三个基础概念构成:

  1. State(状态):描述测试运行的当前状态;
  2. Rule(规则):一段测试代码单元,指定并检查被测系统(system under test, SUT)的某种行为;
  3. Scenario(场景):一组规则的"配方"(recipe),用于构造一个或多个测试。

STest 的设计目标非常明确:为单元测试提供结构,并通过自动生成测试获得手工编写难以企及的覆盖率——例如自动执行成千上万条规则,这是手工编写不可行的。

State:规则与场景共同操作的状态类型

在基于规则的测试中,状态指的是运行中测试的当前状态。STest 将其分为两类:

  • 具体状态(concrete state):被测系统(SUT)的真实状态;
  • 测试状态 / 抽象状态(test state / abstract state):对被测系统状态的抽象建模,用于辅助建模。

从 Rule.hpp 与 Scenario.hpp 的实现可以看出,Rule和Scenario都是以 State 为模板参数的 C++ 模板类(template<typename State>)。也就是说,State 是用户自定义类型——既可以是简单结构,也可以是承载被测组件上下文的测试器(Tester)对象。

在实际的 F Prime 测试中,State 常常就是组件测试器(Tester)本身。例如 Svc/GroundInterface/test/ut/TestMain.cpp 中,Svc::Tester直接作为STest::Rule<Svc::Tester>与STest::RandomScenario<Svc::Tester>的模板参数,规则通过引用该 Tester 来驱动被测组件并收集断言结果。

Rule:行为的最小测试单元

规则的构成

一个规则(Rule)是描述被测系统某种行为的测试代码单元,包含两个要素:

1. 前置条件(precondition):一个谓词函数(只读、返回布尔值),作用于 系统状态,决定该规则何时可以应用。例如:

  • 测试缓冲分配器(buffer allocator)的正常行为时,前置条件可能是"存在可分配的缓冲";
  • 测试"缓冲不可用"这一错误路径时,前置条件则可能是"所有缓冲都已被分配"。

2. 动作(action):分两步完成——(a)驱动被测系统执行某种操作;(b)在给定当前系统状态的前提下,检查结果行为是否符合预期。例如:

  • 分配器的"正常 / OK"规则会检查缓冲确实被分配;
  • "缓冲不可用"规则会检查是否发出了相应的 F' 事件(event)告警。

如何编写规则

在 STest 中,规则是抽象类Rule的派生类,需要覆盖Rule接口中声明的两个纯虚函数。从 Rule.hpp 的源码可见其完整契约:

  • bool precondition(const State& state) = 0;:前置条件谓词,只读地检查状态;
  • void action(State& state) = 0;:执行动作,可修改状态;
  • void apply(State& state):应用规则——先断言前置条件成立(否则输出"precondition failed applying rule <name>"),再调用action(state);
  • 构造函数接收规则名name(const char*),该名称会在场景运行日志中用于标识规则。

一个最小示例(以 GroundInterface 的真实规则为参照,见 GroundInterfaceRules.cpp):

// 定义一条"随机化上行参数"的规则 struct RandomizeRule : public STest::Rule<Tester> { explicit RandomizeRule(const char* const name) : STest::Rule<Tester>(name) {} bool precondition(const Tester& state) override { return true; // 随时可以随机化 } void action(Tester& state) override { state.m_uplink_type = STest::Pick::lowerUpper(0, 1); state.m_uplink_size = STest::Pick::lowerUpper(0, sizeof(state.m_uplink_data) - 1); // ... 其余驱动被测组件、收集断言的逻辑 state.update_header_info(); } };

规则接口的三个抽象方法

Scenario基类(Scenario.hpp)是所有场景的抽象基类,它为所有场景定义了三个必须实现的纯虚接口:

  • void reset_Scenario():重置场景状态;
  • Rule<State>* nextRule_Scenario(State& state):返回下一个要应用的规则(假设isDone()为 false),没有则返回 NULL;
  • bool isDone_Scenario() const:查询场景是否结束。

场景提供统一的run(State& state)入口,其内部runHelper循环执行:重置 → 取下一规则 → 应用规则 → 计数步数 → 判断是否结束,直到没有更多规则可应用为止,并返回实际执行的步数(U32 numSteps)。此外,若当前目录存在show-rules文件,场景在应用每条规则前会打印[Scenario <name>] Applying rule <name>日志,便于调试(见 Scenario.hpp)。

Scenario:规则的"配方"

场景(Scenario)是"用一组规则构造一个或多个测试的配方"。它把规则的选取、重复、组合逻辑抽象出来,让测试作者用很简单的规格构建复杂的测试序列。仓库 Scenario 目录下提供了十余种场景,下面按 README 列举逐一说明(附源码路径)。

序列类场景:固定与随机

  • RuleSequenceScenario:以固定顺序应用一组规则。它由SequenceScenario派生,内部把规则数组逐条包装成RuleScenario(每条规则只应用一次)。
  • RandomScenario:以随机顺序应用一组规则。它由InterleavedScenario派生,内部把每条规则包装成RepeatedRuleScenario(见下文),从而在随机交错的同时允许单条规则被反复命中。

重复类场景:反复应用与条件终止

  • RepeatedRuleScenario:在场景内重复应用一条规则。其isDone_Scenario()恒返回false,只要前置条件成立就持续返回该规则,直到由外层场景(如 Bounded 或 Random 场景)决定何时终止。
  • ConditionalScenario:在某个条件成立时运行一个场景。
  • RepeatedScenario:重复运行一个子场景(内部由IteratedScenario派生,重置时同步重置子场景并读取其结束状态)。

组合类场景:顺序、选择与交错

  • SequenceScenario:创建一系列场景并按顺序运行。
  • SelectedScenario:从一组场景中随机选择一个运行。
  • InterleavedScenario:随机交错运行一组场景——这正是RandomScenario随机化多条规则的基础。
  • ScenarioArray:承载场景数组,供SequenceScenario与InterleavedScenario使用。

迭代与有界类场景:控制运行次数

  • IteratedScenario:迭代一个场景集合。
  • ConditionalIteratedScenario:迭代一个特定场景直到某个条件成立。
  • BoundedIteratedScenario:以固定的迭代次数上界运行一个迭代场景。
  • BoundedScenario:运行场景固定次数(次数上限固定)。
  • RandomlyBoundedScenario:运行场景达到一个随机选择的上界(次数上限随机)。

正是这些迭代类与随机类场景,让测试作者可以用非常简单的规格构造出"探索大量行为"的复杂测试——它们通常会执行成千上万甚至数百万条规则,这在手工编写条件下是不可行的(README 对此有明确说明)。

随机支持:Random 与 Pick

随机场景与随机有界场景都依赖 STest 提供的随机数设施,理解它们有助于正确使用随机化测试:

  • Random.hpp:随机数生成。Random::seed()按"优先从seed文件读取种子,否则用当前时间,并将种子追加写入seed-history文件"的策略播种(详见 Random.hpp 注释);startLength(start, length)、lowerUpper(lower, upper)分别返回区间[start, start+length-1]与[lower, upper]内的随机数;inUnitInterval()返回[0,1]区间的双精度值。底层随机数实现位于 STest/Random 目录的Random.cpp与bsd_random.c。
  • Pick.hpp:便捷取值接口,提供inUnitInterval()、startLength(...)、lowerUpper(...)与any()(任意 U32)。在 GroundInterface 测试中,STest::Pick::lowerUpper(0, 1)被用来随机选择上行类型、随机化上行数据字节(见 GroundInterfaceRules.cpp)。

在 F Prime 中的实际应用

README 明确指出:目前 F Prime 使用 STest 进行组件的单元测试,典型例子包括Svc/GroundInterface与Fw/Logger的测试。其使用模式主要有两种:

  1. 短规则序列:先用若干规则搭建系统状态,再用一条特定规则完成针对性测试;
  2. 随机场景:自动生成更复杂的测试。

完整用例剖析:Svc/GroundInterface 的随机化测试

Svc/GroundInterface/test/ut/TestMain.cpp 是一个教科书级的 STest 组合用法,完整展示了"定义规则 → 组装随机场景 → 套上有界场景 → 运行"的流程:

#define STEP_COUNT 10000 TEST(Nominal, RandomizedGroundIf) { Svc::Tester tester; // 1. 创建规则并放入数组 Svc::RandomizeRule randomize("Randomize"); Svc::DownlinkRule downlink("Downlink"); Svc::FileDownlinkRule filedown("File Down"); Svc::SendAvailableRule sendup(""); STest::Rule<Svc::Tester>* rules[] = { &randomize, &downlink, &filedown, &sendup }; // 2. 构造随机场景:随机选择上述规则反复应用 STest::RandomScenario<Svc::Tester> random("Random Rules", rules, FW_NUM_ARRAY_ELEMENTS(rules)); // 3. 套上有界场景:限定最多运行 10000 步 STest::BoundedScenario<Svc::Tester> bounded("Bounded Random Rules Scenario", random, STEP_COUNT); // 4. 运行并打印实际步数 const U32 numSteps = bounded.run(tester); printf("Ran %u steps.\n", numSteps); }

其依赖的规则定义在 GroundInterfaceRules.cpp 中:每条规则继承STest::Rule<Tester>,重写precondition(此处大多恒为true)与action(在 action 中调用 Tester 的invoke_to_*Port驱动被测组件端口,再用assert_from_*断言输出端口行为,最后clearFromPortHistory()清空端口历史)。

从该用例可以看到 STest 在 F Prime 测试中的完整链路:

  • 状态:Svc::Tester(组件测试器)作为模板参数贯穿所有类;
  • 规则:四条行为各异的规则封装不同上行/下行/文件传输行为;
  • 场景组合:RandomScenario(随机交错)内嵌于BoundedScenario(固定 10000 步上界),一次测试即可自动覆盖大量随机交错的行为序列;
  • 可验证输出:bounded.run(tester)返回实际执行步数,可直接观测测试强度。

此外,Fw/Logger 的单元测试同样采用 STest 规则化测试(见 Fw/Logger 目录下的测试文件),印证了 STest 在 F Prime 组件测试中的通用地位。

构建与集成

STest 以 CMake 静态库形式集成,STest/CMakeLists.txt 将其构建为名为STest的库,编译源包括Random.cpp、bsd_random.c、Pick_default.cpp与Pick.cpp,并把STest目录加入公开头文件搜索路径(target_include_directories(STest PUBLIC ...))。测试代码只需通过#include <STest/Scenario/...>、#include <STest/Pick/Pick.hpp>等头文件引入相应场景与工具即可(如 TestMain.cpp 所示)。

设计要点与最佳实践小结

综合 README 与源码,使用 STest 时有几个值得注意的要点:

  1. 前置条件是只读谓词:precondition(const State&)接收 const 状态,保证规则选择阶段不会污染状态;所有状态变更都应发生在action(State&)中。
  2. 规则命名用于诊断:Rule与Scenario都携带名称;结合show-rules文件(Scenario.hpp),场景会打印每条被应用规则的名称,方便追踪随机化测试的执行路径。
  3. 分层组合场景:单一场景解决单一问题(一次、重复、条件、有界、随机、交错……),复杂测试通过场景嵌套实现——这正是 GroundInterface 用例"RandomScenario 套 BoundedScenario"的做法。
  4. 用随机化换覆盖率:迭代与随机场景天然适合"探索大量行为"的测试需求,执行成千上万步的随机序列是手工用例无法企及的;但要注意配合固定种子(Random::seed()的种子文件机制)以便复现失败用例。
  5. 状态即测试器:在 F Prime 中,State 通常直接使用组件 Tester,规则驱动端口调用并断言端口历史,与 gtest 的断言(ASSERT_TRUE等,见 testing.hpp)无缝衔接。

结语

STest 以"状态 + 规则 + 场景"的三元模型,为 F´ 组件的单元测试提供了既结构化又高度自动化的方案:规则封装行为与预期,场景封装组合与执行策略,随机与迭代场景则把测试强度提升到手工难以达到的量级。通过本文介绍的源码接口与 GroundInterface 真实用例,读者已经具备在自己的 F´ 组件测试中引入 STest 规则化测试的完整路径——定义 Tester 状态、编写 Rule 派生类、用 Bounded/Random 等场景组合运行,即可获得高覆盖、可复现、易维护的组件测试套件。

  • 嵌入式
  • 系统编程

【免费下载链接】fprime

F´ - A flight software and embedded systems framework

项目地址:https://gitcode.com/gh_mirrors/fpri/fprime
点击查看免费下载

相关推荐

上一篇:5分钟上手SWAG:Docker新手必备的HTTPS服务器搭建教程
下一篇:成本降低56%!Qwen2.5-VL-72B-Instruct-quantized.w8a8云部署经济性分析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询