C++测试驱动开发:结合Core Guidelines编写可测性强的代码【单元测试友好】

可测性强的代码需从接口设计、依赖管理、生命周期控制三方面入手,遵循Core Guidelines中IS.5、R.11、C.130等条款,通过纯接口+依赖注入解耦外部状态,禁用裸指针与隐式资源管理,确保函数行为可预测。

可测性强的代码,不是靠加一堆 mock 框架堆出来的,而是从接口设计、依赖管理、生命周期控制这三处下手——Core Guidelines 里 IS.5(接口应最小化依赖)、R.11(避免裸指针传递所有权)、C.130(用策略类或依赖注入解耦行为)这几条,就是最直接的落地依据。

用纯接口 + 依赖注入替代全局单例

测试时最常卡住的地方,是代码偷偷访问了 std::coutstd::time(nullptr)Singleton::instance() 这类不可控外部状态。Core Guidelines 明确反对隐式依赖(见 I.22)。

  • 把所有外部交互抽象成接口,比如 LoggerClockHttpClient,只暴露纯虚函数
  • 构造函数接收这些接口的引用或智能指针(推荐 std::shared_ptr),不 new 不 static
  • 测试时传入 MockLoggerStubClock,无需宏替换或链接期 hack
class PaymentService {
public:
    explicit PaymentService(std::shared_ptr clock,
                           std::shared_ptr logger)
        : clock_{std::move(clock)}, logger_{std::move(logger)} {}

    bool process(const Order& order) {
        logger_->info("Processing order {}", order.id);
        if (clock_->now() > order.deadline) {
            return false;
        }
        // ...
    }

private:
    std::shared_ptr clock_;
    std::shared_ptr logger_;
};

避免裸指针和隐式资源管理

Core Guidelines 的 R.3R.11 强调:裸指针只用于观察,所有权必须由 std::unique_ptrstd::shared_ptr 明确表达。否则单元测试中极易出现 double-free、use-after-free 或无法控制析构时机的问题。

  • 类成员中禁用 SomeClass*,改用 std::unique_ptr(独占)或 std::shared_ptr(共享)
  • 函数返回资源时,不用 new SomeClass,而用 std::make_unique()
  • 测试中可安全地重置 std::unique_ptr,或用 std::weak_ptr 观察生命周期

让函数可预测:输入确定 → 输出确定

Core Guidelines 的 F.24 明确要求:无副作用的函数应声明为 const;有副作用的函数必须清晰表达其影响。否则测试断言会变成“猜行为”。

  • 纯计算函数(如 calculateTax(double amount, const TaxRate& rate))必须是 noexcept 且不读写成员变量
  • 修改状态的函数(如 void addOrder(Order&& o))要显式命名,并在头文件注释中标明是否线程安全、是否会抛异常
  • 避免在构造函数里做 I/O 或网络调用——这会让 TEST_F 初始化直接失败,且无法 patch

测试桩(stub)与模拟(mock)的选择边界

GoogleTest 的 MOCK_METHOD 很方便,但 Core Guidelines 的 C.131 提醒:过度模拟说明接口粒度太细或职责不清。优先用 stub,仅当需要验证调用顺序/次数时才上 mock。

  • Stub:返回固定值,比如 StubClock::now() { return std::chrono::system_clock::from_time_t(1717027200); }
  • Mock:只对真正关键的协作行为打桩,例如“支付失败时是否触发补偿回调”,而不是“是否调用了 logger->e

    rror
  • 注意:mock 对象本身可能引入静态生命周期(如全局 MockHttpClient 实例),违反 I.3(接口不应依赖初始化顺序)

最难的其实不是写测试,而是让被测代码不抗拒测试——它体现在你第一次尝试给某个函数写单元测试时,是不是得先注释掉三行日志、临时替换两个单例、再把时间函数用宏包起来。如果答案是“是”,那不是测试框架不行,是代码没按 Core Guidelines 的约束去组织。