如何正确结合GMock、虚函数与Boost MSM?嵌入式测试难题求解
解决方案:避免生产代码生成VTable的Boost MSM测试方案
针对嵌入式场景下Boost MSM状态机的测试需求,以下几个方案可以在不引入生产代码VTable的前提下,完成状态机对静态类X方法调用逻辑的验证:
方案1:模板注入(Policy-Based Design)
将状态机设计为模板类,把负责UART命令生成的类作为模板参数传入。生产环境使用原静态类X,测试环境使用带GMock虚方法的Mock类——由于模板实例化是编译期决定的,生产代码中只会实例化使用静态类X的版本,完全不会生成VTable。
代码示例:
状态机模板定义
#include <boost/msm/back/state_machine.hpp> #include <boost/msm/front/state_machine_def.hpp> // 原静态类X(生产代码用) class X { public: static void send_init_cmd() { /* 生产环境UART命令逻辑 */ } static void send_reset_cmd() { /* 生产环境UART命令逻辑 */ } }; // 状态机前端定义(模板化) template<typename CmdGenerator> struct StateMachineFront : boost::msm::front::state_machine_def<StateMachineFront<CmdGenerator>> { // 状态定义 struct InitState : boost::msm::front::state<> {}; struct ResetState : boost::msm::front::state<> {}; // 初始状态 using initial_state = InitState; // 事件定义 struct ResetEvent {}; // 动作:调用CmdGenerator的静态方法 struct CallInitCmd { template<typename Event, typename FSM, typename SourceState, typename TargetState> void operator()(const Event&, FSM&, SourceState&, TargetState&) { CmdGenerator::send_init_cmd(); } }; struct CallResetCmd { template<typename Event, typename FSM, typename SourceState, typename TargetState> void operator()(const Event&, FSM&, SourceState&, TargetState&) { CmdGenerator::send_reset_cmd(); } }; // 状态转换表 using transition_table = boost::mpl::vector< boost::msm::front::Row<InitState, ResetEvent, ResetState, CallResetCmd, boost::msm::front::none>, boost::msm::front::Row<ResetState, boost::msm::front::none, InitState, CallInitCmd, boost::msm::front::none> >; }; // 生产环境状态机实例 using ProductionStateMachine = boost::msm::back::state_machine<StateMachineFront<X>>;
测试代码(使用GMock)
#include <gmock/gmock.h> // 测试用Mock类(仅在测试代码中编译) class MockCmdGenerator { public: MOCK_STATIC_METHOD0(send_init_cmd, void()); MOCK_STATIC_METHOD0(send_reset_cmd, void()); }; // 测试环境状态机实例 using TestStateMachine = boost::msm::back::state_machine<StateMachineFront<MockCmdGenerator>>; TEST(StateMachineTest, TestCmdCalls) { TestStateMachine fsm; fsm.start(); // 验证初始状态调用send_init_cmd EXPECT_CALL(MockCmdGenerator, send_init_cmd()).Times(1); fsm.process_event(typename StateMachineFront<MockCmdGenerator>::ResetEvent{}); // 验证状态转换后调用send_reset_cmd EXPECT_CALL(MockCmdGenerator, send_reset_cmd()).Times(1); fsm.process_event(typename StateMachineFront<MockCmdGenerator>::ResetEvent{}); }
方案2:条件编译切换实现
通过预编译宏区分生产和测试环境,生产环境直接调用静态类X的方法,测试环境调用全局Mock对象的方法。这种方式完全避免了模板,代码更简洁,且生产代码无任何虚函数开销。
代码示例:
#include <boost/msm/back/state_machine.hpp> #include <boost/msm/front/state_machine_def.hpp> // 原静态类X class X { public: static void send_init_cmd() { /* 生产逻辑 */ } static void send_reset_cmd() { /* 生产逻辑 */ } }; // 测试用Mock(仅测试环境编译) #ifdef TEST_BUILD #include <gmock/gmock.h> class MockCmdGenerator { public: MOCK_METHOD0(send_init_cmd, void()); MOCK_METHOD0(send_reset_cmd, void()); }; extern MockCmdGenerator g_mock_cmd_gen; #endif // 状态机前端定义(非模板) struct StateMachineFront : boost::msm::front::state_machine_def<StateMachineFront> { struct InitState : boost::msm::front::state<> {}; struct ResetState : boost::msm::front::state<> {}; using initial_state = InitState; struct ResetEvent {}; struct CallInitCmd { template<typename Event, typename FSM, typename SourceState, typename TargetState> void operator()(const Event&, FSM&, SourceState&, TargetState&) { #ifdef TEST_BUILD g_mock_cmd_gen.send_init_cmd(); #else X::send_init_cmd(); #endif } }; struct CallResetCmd { template<typename Event, typename FSM, typename SourceState, typename TargetState> void operator()(const Event&, FSM&, SourceState&, TargetState&) { #ifdef TEST_BUILD g_mock_cmd_gen.send_reset_cmd(); #else X::send_reset_cmd(); #endif } }; using transition_table = boost::mpl::vector< boost::msm::front::Row<InitState, ResetEvent, ResetState, CallResetCmd, boost::msm::front::none>, boost::msm::front::Row<ResetState, boost::msm::front::none, InitState, CallInitCmd, boost::msm::front::none> >; }; using StateMachine = boost::msm::back::state_machine<StateMachineFront>;
测试代码中初始化全局Mock
#include <gmock/gmock.h> MockCmdGenerator g_mock_cmd_gen; TEST(StateMachineTest, TestCmdCalls) { StateMachine fsm; fsm.start(); EXPECT_CALL(g_mock_cmd_gen, send_init_cmd()).Times(1); fsm.process_event(StateMachineFront::ResetEvent{}); EXPECT_CALL(g_mock_cmd_gen, send_reset_cmd()).Times(1); fsm.process_event(StateMachineFront::ResetEvent{}); }
方案3:Boost MSM动作适配器
利用Boost MSM的动作机制,将方法调用封装为可替换的函数对象,生产环境绑定静态类X的方法,测试环境绑定Mock的方法。这种方式无需修改状态机的核心结构,仅需替换动作实现。
代码示例:
#include <boost/msm/back/state_machine.hpp> #include <boost/msm/front/state_machine_def.hpp> #include <functional> // 原静态类X class X { public: static void send_init_cmd() { /* 生产逻辑 */ } static void send_reset_cmd() { /* 生产逻辑 */ } }; // 动作适配器:封装可替换的命令调用逻辑 struct CmdActions { static std::function<void()> send_init_cmd; static std::function<void()> send_reset_cmd; }; // 生产环境初始化动作 #ifndef TEST_BUILD std::function<void()> CmdActions::send_init_cmd = [](){ X::send_init_cmd(); }; std::function<void()> CmdActions::send_reset_cmd = [](){ X::send_reset_cmd(); }; #endif // 状态机前端定义 struct StateMachineFront : boost::msm::front::state_machine_def<StateMachineFront> { struct InitState : boost::msm::front::state<> {}; struct ResetState : boost::msm::front::state<> {}; using initial_state = InitState; struct ResetEvent {}; struct CallInitCmd { template<typename Event, typename FSM, typename SourceState, typename TargetState> void operator()(const Event&, FSM&, SourceState&, TargetState&) { CmdActions::send_init_cmd(); } }; struct CallResetCmd { template<typename Event, typename FSM, typename SourceState, typename TargetState> void operator()(const Event&, FSM&, SourceState&, TargetState&) { CmdActions::send_reset_cmd(); } }; using transition_table = boost::mpl::vector< boost::msm::front::Row<InitState, ResetEvent, ResetState, CallResetCmd, boost::msm::front::none>, boost::msm::front::Row<ResetState, boost::msm::front::none, InitState, CallInitCmd, boost::msm::front::none> >; }; using StateMachine = boost::msm::back::state_machine<StateMachineFront>;
测试代码中替换动作
#include <gmock/gmock.h> class MockCmdGenerator { public: MOCK_METHOD0(send_init_cmd, void()); MOCK_METHOD0(send_reset_cmd, void()); }; TEST(StateMachineTest, TestCmdCalls) { MockCmdGenerator mock; // 替换动作实现为Mock调用 CmdActions::send_init_cmd = [&mock](){ mock.send_init_cmd(); }; CmdActions::send_reset_cmd = [&mock](){ mock.send_reset_cmd(); }; StateMachine fsm; fsm.start(); EXPECT_CALL(mock, send_init_cmd()).Times(1); fsm.process_event(StateMachineFront::ResetEvent{}); EXPECT_CALL(mock, send_reset_cmd()).Times(1); fsm.process_event(StateMachineFront::ResetEvent{}); }
方案选择建议
- 如果项目已经采用模板化设计,优先选方案1,它的扩展性最好,测试和生产代码完全解耦。
- 如果追求代码简洁、最小改动,选方案2,仅通过宏切换实现,生产代码无额外开销。
- 如果不想修改状态机核心结构,选方案3,通过函数对象动态替换动作,灵活性较强。
内容的提问来源于stack exchange,提问作者Daniel S
相关产品推荐
相关产品推荐

