当前位置: 首页 > news >正文

CppCon 2017 学习:Mocking Frameworks Considered

当然可以,下面是对 Fowler 的 Whiskey-Store 示例。

Fowler 的 Whiskey-Store 示例(坏设计)

贴出的类图是 Martin Fowler 在《重构》书中使用的一个教学用反面案例故意设计得不合理),用来说明如何通过重构改善代码设计。

类图解析(你的 Mermaid 图)

fillFrom
Order
-what: string
-howmany: int
+order(what: string, howmany: int)
+fillFrom(wh: Warehouse)
+filled()
Warehouse
+hasInventory(what: string, howmany: int)
+add(what: string, howmany: int)
+remove(what: string, howmany: int)
+getInventory(what: string)

为何说是“坏设计”?

1. 类的职责不清晰(违反单一职责原则)

  • Order 这个类不仅代表订单,还负责自己去调用仓库去处理库存。
  • 它不只是数据对象,还承担业务逻辑(库存检查、扣减等),职责太多。

2. 耦合过高

  • Order 类直接调用 Warehouse 的方法(hasInventory()remove())。
  • 如果以后库存不再由 Warehouse 处理,修改成本就高。

3. 违反迪米特法则(Law of Demeter)

  • “最少知道原则”:一个类应该尽量少知道其他类的内部结构。
  • Order 知道太多关于 Warehouse 的细节。

4. 难以扩展

  • 现在的 fillFrom() 方法只支持一个仓库,无法轻松支持:
    • 多个仓库轮询
    • 库存不足时的部分发货
    • 优先级策略、折扣策略等

更好的设计建议

应该增加一个服务类来处理“订单如何从仓库发货”的逻辑,订单和仓库只保留各自的职责。

改进后的类图:

Order
-what: string
-howmany: int
+filled()
Warehouse
+hasInventory(what: string, howmany: int)
+remove(what: string, howmany: int)
+add(what: string, howmany: int)
OrderService
+fill(order: Order, warehouse: Warehouse)

总结

项目坏设计版本好设计版本
类职责混合业务逻辑和数据职责清晰,分离业务逻辑
耦合程度Order 和 Warehouse 强耦合使用中间服务类降低耦合
可扩展性扩展困难通过服务类轻松扩展策略
可读性命名、结构都容易让人迷惑清晰,易于维护
#两个 Mermaid 图(状态图 + 时序图)展示的是 Order 对象的期望行为(intended behavior),用于理解其在“填充订单”流程中的状态变化和与 Warehouse(仓库)的交互。
这部分内容通常用于单元测试设计,特别是带有副作用或状态变化的对象(Stateful APIs)

第一部分:状态图(stateDiagram)

wh.hasInventory
Condition = true
wh.remove
filled = true
Condition = false
filled = false
Start
Checking
Removing
FilledTrue
FilledFalse

这是订单执行 fillFrom(wh) 方法时的状态转移流程图

  1. Start:开始执行 fillFrom()
  2. Checking:调用 wh.hasInventory(what, howmany) 检查仓库是否有库存。
  3. 根据检查结果:
    • 有库存(true)
      • 进入 Removing 状态,调用 wh.remove(what, howmany) 扣除库存;
      • 设置 filled = true,订单被成功填充。
    • 无库存(false)
      • 设置 filled = false,订单未被填充。

这是一个典型的有状态流程,适合用于行为测试或状态验证。

第二部分:时序图(sequenceDiagram)中文解析

order SUT Warehouse Fill hasInventory(TALISKER, 50) true remove(TALISKER, 50) setFilled order SUT Warehouse

中文解释:

这是一个方法调用的时间顺序图(Sequence Diagram),用于验证 Order(或它背后的服务逻辑)与 Warehouse 的交互。

参与者:
  • order: 外部调用者或测试对象
  • SUT(System Under Test):测试中的对象,可能是 OrderOrderService
  • Warehouse: 依赖的仓库对象(DOC = Depended-on Component
调用流程:
  1. order 调用 SUT.Fill()
  2. SUTWarehouse 查询库存:hasInventory("TALISKER", 50)
  3. 如果返回 true
    • 调用 remove("TALISKER", 50) 从库存中移除;
    • 调用 setFilled() 设置订单已填充;
  4. 返回结果给 order

单元测试中的应用:Mock 和验证交互

这个图用于说明如何测试 SUT(比如 Order)是否正确调用了 Warehouse,这就是交互测试(Interaction Testing)

重点:

  • 使用 Mock 对象 替代真实的 Warehouse
  • 验证是否:
    • 调用了 hasInventory()
    • 在库存充足时调用了 remove()
    • 正确地设置了订单的 filled 状态;
    • 调用顺序是正确的(先检查库存,后移除);
  • 这就是所谓的:

    “验证 Order(SUT)对 Warehouse(DOC)的调用顺序和使用方式”

总结:

内容解释
stateDiagram显示订单在不同库存条件下的状态变化流程
sequenceDiagram显示 OrderWarehouse 之间的调用时序与交互
Mock 的用途模拟 Warehouse,验证其被正确调用,不用真实对象
“SUT”和“DOC”的含义SUT = 测试的对象(如 Order),DOC = 被依赖的组件(如仓库)
测试重点验证行为逻辑 + 方法调用顺序 + 状态变化是否符合预期
你这段内容的重点是说明测试含有依赖组件(DOC)的系统(SUT),并用 Mermaid 图形象地表达测试流程及交互关系。下面帮你梳理总结和理解:

Mermaid 图结构简要说明

initialize: Direct Inputs
Control Points
with return value
Direct Outputs:
Observation Points
Get State
Indirect Output:
Observation Point
Get Something (return)
Indirect Input:
Control Point
Do Something (no return)
Indirect Output:
Observation Point
Setup
SUT
Exercise
Verify
Teardown
Fixture_DOC
  • Setup 初始化测试环境,传入直接输入(控制点)
  • SUT(System Under Test)即被测试系统
  • Exercise 是测试操作调用 SUT
  • Verify 验证测试结果和系统状态
  • Teardown 测试清理阶段
  • Fixture_DOC(Dependent Other Component)是 SUT 依赖的其他组件,测试时用以模拟或控制
  • 图中用“Direct Inputs”、“Indirect Inputs”、“Observation Points”等术语区分输入输出类型

术语解释

  • DOC:Dependent Other Component,依赖的外部组件或系统,通常被模拟(mock)以实现测试隔离
  • SUT:System Under Test,测试对象
  • Direct Inputs (Control Points):直接控制 SUT 的输入
  • Indirect Inputs (Control Point):通过依赖组件间接控制的输入
  • Direct Outputs (Observation Points):直接从 SUT 可观察的输出
  • Indirect Outputs (Observation Points):通过依赖组件间接观察的输出
  • Get Something (with return value):依赖组件返回的数据
  • Do Something (no return value):依赖组件执行的动作但无返回

理解重点

  • 测试设计通过分离 SUT 和它的依赖(DOC),实现解耦
  • 依赖组件的行为通过间接输入/输出被控制和观察
  • 测试通过直接调用和间接调用,验证 SUT 的行为和状态
  • 这种设计有助于编写可维护且可靠的测试,特别是面对复杂依赖时

测试流程详细说明

1. Setup(测试初始化)

  • 作用:搭建测试环境,准备所有必要的输入和依赖。
  • 具体内容
    • 初始化直接输入(Direct Inputs),即测试代码将直接传给被测系统(SUT)的参数或配置。
    • 设置控制点(Control Points),用来控制测试的条件和行为。
  • 目的是让测试有一个确定且受控的起点,保证测试的可重复性。

2. SUT(被测试系统)

  • 作用:被测的核心系统或模块,执行被测试的业务逻辑。
  • 特点
    • 依赖于外部组件(Fixture_DOC)来完成部分功能。
    • 接受来自 Setup 的直接输入,以及来自依赖组件的间接输入。
  • 这是测试的目标对象,所有测试操作都围绕它展开。

3. Exercise(测试执行)

  • 作用:执行具体的测试动作,比如调用 SUT 的接口或方法。
  • 具体行为
    • 向 SUT 发出操作请求。
    • SUT 返回结果(with return value)。
  • 通过模拟实际使用场景触发系统逻辑,产生输出供后续验证。

4. Verify(测试验证)

  • 作用:对测试执行的结果进行验证,判断是否符合预期。
  • 具体工作
    • 观察 SUT 的直接输出(Direct Outputs)——比如返回值、状态变化等。
    • 获取 SUT 的内部状态(Get State),确认系统内部是否正确更新。
    • 同时也可以观察依赖组件的输出,验证它们是否被正确调用和响应。
  • 这是判断测试是否通过的关键阶段。

5. Teardown(测试清理)

  • 作用:清理测试环境,释放资源,保证测试间相互独立。
  • 工作内容
    • 恢复被测试系统和依赖组件到初始状态。
    • 关闭或重置测试期间创建的对象和连接。

6. Fixture_DOC(依赖组件)

  • 作用:模拟或封装 SUT 的依赖系统,帮助测试隔离和控制复杂依赖。
  • 交互细节
    • Indirect Input:对依赖组件下达控制命令(Do Something),不返回值,用来模拟依赖行为。
    • Indirect Output:从依赖组件获取数据(Get Something)或观察依赖状态,辅助验证。
  • 通过控制和观察依赖,帮助检测 SUT 在各种依赖响应情况下的表现。

总结

这套测试设计遵循了“解耦依赖”的原则:

  • Setup 提供确定的起点和直接控制。
  • Exercise 触发系统动作。
  • Verify 多角度验证系统行为和状态。
  • Fixture_DOC 模拟依赖,使测试更可控、准确。
  • Teardown 保证测试环境整洁。

为什么在测试中需要用到 stub 或 mock的原因,核心点可以总结和解释如下:

为什么需要 Stub/Mock?

  1. 真实对象行为不可预测(Nondeterministic)
    真实对象可能产生不稳定、随机的结果,比如股票行情数据、网络请求的状态等,这会导致测试结果不稳定。
  2. 真实对象难以搭建(Difficult to Set Up)
    真实对象可能需要复杂的环境或者大量资源来运行,比如硬件设备、数据库或远程服务,测试环境难以复现。
  3. 真实对象行为难以触发(Hard to Trigger)
    真实对象的某些行为或错误状态很难在测试中触发,比如网络故障、内存溢出、特定异常等。
  4. 真实对象运行缓慢(Slow)
    真实对象执行速度慢会拖慢测试速度,不利于频繁执行单元测试。
  5. 真实对象是用户界面(User Interface)
    界面交互难以自动化,且界面行为不适合用作底层逻辑测试。
  6. 测试需要验证真实对象的使用情况
    比如确认某个回调函数是否被调用,或者确认接口调用顺序和参数。
  7. 真实对象尚不存在(Not Yet Implemented)
    测试先于依赖组件的开发,依赖未完成时需要模拟其行为。

示例说明

  • 股票行情推送常有不确定数据,难以预测结果。
  • 真实网络错误或资源限制可能导致不同的异常。
  • 硬件动作(如马达运动)难以用模拟环境稳定复制。
  • 用户本人交互难以在自动化测试中完全复现。
  • 需要确认回调是否执行,例如事件监听器。
  • 当与其他团队或新硬件系统协作时,依赖组件可能还未完成,必须用模拟代替。

总结

使用 stub 和 mock 的目的是让测试变得:

  • 可控:排除不可预见因素影响,测试更稳定。
  • 高效:避免复杂依赖和慢速操作。
  • 可验证:能验证交互细节和调用顺序。
  • 提前测试:即使依赖未完成,也能开展测试。

“Test double patterns”实际上就是测试替身(Test Double)的各种形式和目的,主要用来解决两个关键问题:

1. 如何在依赖代码不可用时,独立验证逻辑?

  • 当你要测试的代码依赖某些外部组件或模块,但这些依赖:
    • 尚未开发完成,
    • 不稳定,
    • 行为不可预测,
    • 难以搭建,
      这时候就需要用**测试替身(Test Doubles)**来模拟这些依赖,确保测试的逻辑独立、可靠。
  • 通过使用 Stub、Mock、Fake、Spy 等不同类型的测试替身,替代真实依赖,模拟特定的行为和响应,保证你能专注于验证目标代码本身的业务逻辑。

2. 如何避免测试变慢?

  • 真实依赖可能涉及网络请求、数据库操作、文件IO、硬件设备等,执行速度慢。
  • 使用测试替身可以:
    • 快速返回预设的响应,
    • 避免真正调用外部系统,
    • 极大提升测试执行速度,
    • 使得单元测试能频繁、快速执行,方便持续集成。

总结

**测试替身模式(Test Double Patterns)**的核心价值是:

  • 隔离依赖:独立验证代码逻辑,不受外部因素干扰。
  • 提升效率:加速测试运行,避免慢依赖。
  • 控制场景:轻松模拟各种边界条件和异常情况,方便全面测试。

Mock Object Patterns 主要解决的问题

1. 如何对 SUT(被测试系统)间接输出进行行为验证?

SUT 有时候不会直接返回结果,而是通过调用其他组件(依赖)来完成某些操作。Mock 对象充当这些依赖,帮我们检测:

  • SUT 是否调用了依赖的正确方法,
  • 调用了多少次,
  • 传递了什么参数。
    这就是行为验证(Behavior Verification),通过观察与 Mock 的交互确认 SUT 的行为是否符合预期。

2. 如何在依赖外部组件的情况下,独立验证业务逻辑?

业务逻辑往往依赖其他组件的输入(间接输入),这些依赖可能难以搭建或不稳定。Mock 对象:

  • 模拟这些依赖的行为和返回值,
  • 让我们在隔离环境下,专注于验证 SUT 自身逻辑,
  • 免去对外部组件的真实依赖,提高测试的可靠性和执行速度。

简单说:

  • 行为验证是用 Mock 监视依赖被正确调用。
  • 独立验证是用 Mock 替代依赖,模拟输入,专注测试核心逻辑。

“Configurable Test Double”就是指那种可以在测试准备阶段(fixture setup)配置好行为和期望的测试替身,比如设置它返回什么数据或者期望被调用多少次、参数是什么。

关键点:

  • 配置复用:Test Double 在测试开始前配置好返回值或调用期望,可以在多个测试中复用相同配置。
  • 方便测试:通过配置减少每次测试重复写代码,让测试更简洁、可维护。
  • 滥用风险:有些 Mock 框架过度依赖这种“配置式”Mock,导致测试变得复杂且难以理解,尤其是当配置很复杂时,测试变得难以维护。

总结:

Configurable Test Double 很方便,但要合理使用,避免复杂配置堆积,影响测试清晰度和稳定性。

关于Mocking Framework的核心功能和设计思想,简单来说:

什么是 Mocking Framework?

1. 依赖注入 (Dependency Injection)

  • 通常需要通过依赖注入,将被测试代码(SUT)中对真实依赖(DOC)的引用替换成测试替身(Test Double)。
  • 这一步有时需要改造代码以支持依赖注入,也就是“引入接缝(Introduce Seam)”,方便替换。

2. 替换真实依赖(DOC)

  • Mock 框架帮你自动替换依赖,用测试替身代替真实的依赖组件(DOC)。
  • 这样测试时可以控制依赖的行为。

3. 伪造依赖结果

  • 框架可以让你轻松伪造依赖的函数返回值,帮助 SUT 在测试中获得期望的输入。

4. 跟踪调用和参数

  • Mock 对象会记录 SUT 调用的函数名、调用顺序和传入参数,方便后续验证。

5. 验证调用顺序和参数匹配

  • 可以验证 SUT 是否按照预期顺序调用了依赖的函数,参数是否正确。

总结:

Mocking Framework 是测试替身的自动生成和管理工具,核心价值是:

  • 简化依赖注入与替换,
  • 方便伪造和控制依赖行为,
  • 自动跟踪和验证调用细节,
  • 支持行为验证和交互测试。

Introducing Seams(引入接缝)的核心点:

什么是 Seam?

  • Seam(接缝) 是代码中可以改变行为的“切入点”,不用修改原代码逻辑,就能插入或替换行为。

常见的Seam类型:

  1. Object Seam(对象接缝)
    • 通过接口或继承的虚方法实现。
    • 把依赖(DOC)和测试替身(Test Double)通过构造函数参数传入。
    • 测试时传入替身,运行时传入真实依赖。
  2. Compile Seam(编译接缝)
    • 利用模板参数。
    • 生产代码里,模板参数是默认的真实依赖。
    • 测试时用模板参数替换为测试替身。
  3. Linker Seam / Preprocessor Seam(链接器接缝 / 预处理器接缝)
    • 用链接器替换符号或用预处理器条件编译。
    • 常作为“最后手段”,尤其是 C 语言函数的替代方案。

为什么重要?

  • 允许我们在不动现有代码的情况下插入测试替身。
  • 让代码更灵活,更容易做单元测试,尤其是遗留代码。

你这两个 mermaid 类图展示了通过 Extract Interface Refactoring(提取接口重构) 实现 可 Mock 化(Mockability) 的关键步骤,非常标准且清晰。

第一张图(重构前):

fillFrom
Order
-what: string
-howmany: int
+order(what: string, howmany: int)
+fillFrom(wh: Warehouse)
+filled()
Warehouse
+hasInventory(what: string, howmany: int)
+add(what: string, howmany: int)
+remove(what: string, howmany: int)
+getInventory(what: string)

问题:

  • Order 直接依赖具体类 Warehouse
  • 在测试中不能用 MockWarehouse 替代,耦合紧密,不可 Mock

第二张图(重构后):

order
fillFrom
UnitTest
+order(what: string, howmany: int)
Order
-what: string
-howmany: int
+order(what: string, howmany: int)
+fillFrom(wh: IWarehouse)
+filled()
IWarehouse
+hasInventory(what: string, howmany: int)
+add(what: string, howmany: int)
+remove(what: string, howmany: int)
+getInventory(what: string)
Warehouse
+hasInventory(what: string, howmany: int)
+add(what: string, howmany: int)
+remove(what: string, howmany: int)
+getInventory(what: string)
MockWarehouse
+hasInventory(what: string, howmany: int)
+add(what: string, howmany: int)
+remove(what: string, howmany: int)
+getInventory(what: string)

优点:

  • Order 不再依赖具体类 Warehouse,而是依赖接口 IWarehouse
  • MockWarehouse 实现了 IWarehouse,可以用于单元测试。
  • 这使得 Order 更容易测试(可替换、可隔离、可模拟),同时遵循了依赖倒置原则(DIP)

总结:

原则/技术名说明
Extract Interface提取公共接口,减少耦合
Mock Object模拟外部依赖以隔离测试
DIP(依赖倒置)高层模块依赖抽象,不依赖具体类
Test Double模拟或伪造依赖的通用术语
Seam在此处引入 mock,用于测试控制点

你提供的 C++ 代码展示了一个非常清晰的 “Extract Interface” 重构 实例,用于实现 可测试性和解耦,尤其是在单元测试中引入 mock 的典型场景。

你做了什么?

你将原本紧耦合的代码:

struct Order {void fill(Warehouse& warehouse); // 直接依赖具体类
};

重构为:

struct Order {void fill(WarehouseInterface& warehouse); // 依赖接口
};

并提取出接口:

struct WarehouseInterface {virtual ~WarehouseInterface() {}virtual int getInventory(std::string const & s) const = 0;virtual void remove(std::string const & s, int i) = 0;virtual void add(std::string const & s, int i) = 0;virtual bool hasInventory(std::string const & s, int i) const = 0;
};

让原本的 Warehouse 成为接口的实现:

struct Warehouse : WarehouseInterface {// 实现所有接口方法
};

为什么这么做?

原因解释
测试解耦Order 不再依赖具体的 Warehouse,因此可以传入一个 MockWarehouse 来测试 Order 的逻辑。
引入 Mock/Stub你可以为 WarehouseInterface 创建一个 MockWarehouse 来模拟行为、跟踪调用。
遵循 SOLID 中的 DIPOrder(高层模块)依赖的是抽象(接口),而不是低层细节(具体类)。
单一职责与模块化设计更清晰的边界,有助于代码维护和扩展

Mock 示例(简化版):

struct MockWarehouse : WarehouseInterface {bool wasRemoveCalled = false;int getInventory(const std::string& s) const override {return 100;}void remove(const std::string& s, int i) override {wasRemoveCalled = true;}void add(const std::string& s, int i) override {}bool hasInventory(const std::string& s, int i) const override {return true;}
};
// 测试
Order o;
MockWarehouse mock;
o.fill(mock);
assert(mock.wasRemoveCalled);

总结关键术语

名称意义
Interface Extraction提取接口用于打断依赖,方便替换
WarehouseInterface提取出的 seam,用于解耦
MockWarehouse测试用的 double,实现接口,用于模拟依赖
Dependency Injection通过参数将依赖注入,而非内部创建
DIP(依赖倒置原则)高层依赖抽象,低层实现接口

代码展示了 使用 Fake Object 测试意图行为(intended behavior) 的经典做法,我来帮你总结和解释下其中的概念、关键点和作用。

目标:验证 Order 的预期行为(intended behavior)

你希望测试:

如果仓库中没有足够的库存(即空仓库),调用 order.fill() 后,Order 不应该被填充

解决方法:创建一个 Fake 仓库类

你定义了一个空的仓库实现:

struct EmptyWarehouse : WarehouseInterface {void add(std::string const &, int) override {}int getInventory(std::string const &) const override { return 0; }bool hasInventory(std::string const &, int) const override { return false; }void remove(std::string const &, int) override {}
};

这个类:

  • 实现了接口 WarehouseInterface
  • 明确表示“没有库存”,即“库存为0”、“没有任何商品”
  • 是一个 Fake Object(并非 Mock,也非 Stub):它实现了正确的业务规则的简化逻辑

测试代码解释

void OrderFillFromWarehouse() {Order order(TALISKER, 50);EmptyWarehouse warehouse{};order.fill(warehouse);ASSERT(not order.isFilled());
}

这个测试用例:

  • 使用 EmptyWarehouse 模拟实际仓库
  • 检查在库存不足的情况下,order.isFilled() 返回 false
  • 验证 Order 的逻辑是正确的(它依赖仓库回答是否有货)

Fake 的意义 vs Stub/Mock

类型用途特点
Stub返回预设值不检查交互,仅提供数据
Fake实现简化逻辑具备一定行为逻辑,如 EmptyWarehouse
Mock验证行为跟踪调用/断言交互是否发生
你这里用的是 Fake:行为受控、但符合逻辑规则。

总结

  • Order 遵循依赖倒置,通过接口解耦
  • EmptyWarehouse 是一个 Fake,用于测试 Order 的行为
  • 你测试的是:在仓库无货时,订单不会被填充
  • 你无需依赖真实的仓库数据、状态或副作用

C++ 中为单元测试引入 Mock 的准备过程,特别是通过 提取接口(Extract Interface) 与使用 Mock 框架(如 Google Mock / Trompeloeil)。我来详细解释关键要点。

背景:为什么要 Extract Interface?

你有一个类 Warehouse,它在生产代码中被广泛使用,现在你想对依赖它的 Order 类进行单元测试。但是:

  • Warehouse 太复杂,或者依赖外部资源(比如数据库或网络)
  • 你不想在测试中使用真实对象
  • 你想测试 Order 的行为是否 正确调用了 Warehouse 的方法(行为验证)

解法:提取接口 WarehouseInterface

struct WarehouseInterface {virtual int getInventory(std::string const & s) const = 0;virtual void remove(std::string const & s, int i) = 0;virtual void add(std::string const & s, int i) = 0;virtual bool hasInventory(std::string const & s, int i) const = 0;
};

现在你可以让:

  • Warehouse 实现这个接口(用于生产环境)
  • MockWarehouse 实现这个接口(用于测试)

Mock 实现:两种框架风格

你展示了两种语法,其实分别是:

1. Google Mock 风格(MOCK_METHOD, MOCK_CONST_METHOD):

struct MockWarehouse : WarehouseInterface {MOCK_CONST_METHOD1(getInventory, int(std::string const&));MOCK_METHOD2(remove, void(std::string const&, int));MOCK_METHOD2(add, void(std::string const&, int));MOCK_CONST_METHOD2(hasInventory, bool(std::string const&, int));
};
  • 使用 Google Mock 宏
  • 在测试中你可以设置预期调用、断言调用顺序等
  • 例如:EXPECT_CALL(mock, hasInventory("item", 50)).WillOnce(Return(true));

2. Trompeloeil 风格(MAKE_MOCK, MAKE_CONST_MOCK):

struct MockWarehouse : WarehouseInterface {MAKE_CONST_MOCK1(getInventory, int(std::string const&), override);MAKE_MOCK2(remove, void(std::string const&, int), override);MAKE_MOCK2(add, void(std::string const&, int), override);MAKE_CONST_MOCK2(hasInventory, bool(std::string const&, int), override);
};
  • Trompeloeil 是另一个现代的 C++ mocking 框架,语法略有不同
  • 特点:使用 C++14/17 的特性,报错信息更清晰,类型安全强

模拟场景:测试 Order 使用 MockWarehouse 的行为

TEST(OrderTest, FillsWhenInventoryIsAvailable) {MockWarehouse mock;EXPECT_CALL(mock, hasInventory("TALISKER", 50)).WillOnce(Return(true));EXPECT_CALL(mock, remove("TALISKER", 50));Order order("TALISKER", 50);order.fill(mock);ASSERT_TRUE(order.isFilled());
}
  • 测试验证了:
    • Order 是否调用了正确的 hasInventoryremove 方法
    • Order 的状态最终是否为 filled

总结:引入 Mock 的步骤

步骤目的
提取接口 WarehouseInterface为生产对象和测试替身解耦
使用 Mock 框架(Google Mock / Trompeloeil)自动实现接口,用于测试
编写期望调用 (EXPECT_CALL)验证 SUT 与依赖的交互
在测试中注入 Mock使用依赖注入实现可测性

这句代码:

EXPECT_CALL(mock, hasInventory("item", 50)).WillOnce(Return(true));

是使用 Google Mock 框架写的一行行为期望设置(expectation)。它的作用可以分解如下 :

作用详解:

1. 指定调用期望:

这句代码表示:

在测试执行过程中,期望 mock 对象上的 hasInventory("item", 50) 方法被调用一次。

如果这个调用没发生,测试就会失败。

2. 指定返回值:

当这个方法被调用时,返回 true

这相当于“伪造”了一个依赖组件的行为 —— 我们模拟了 Warehouse 的行为,使它声称有足够库存。

3. 限制调用次数:

使用 WillOnce(...) 表示:

只允许调用一次,如果多次调用也会导致测试失败(除非你用 WillRepeatedly(...))。

示例上下文:

假设你在测试一个类 Order,它会调用 hasInventory() 来检查库存:

Order order("item", 50);
order.fill(mock);
ASSERT_TRUE(order.isFilled());

你希望测试 Order::fill() 是否能:

  1. 检查仓库有没有库存(hasInventory(...)
  2. 在库存充足时正确设置 filled = true

为什么有用?

这段 EXPECT_CALL(...).WillOnce(...) 的意义在于:

  • 它将测试 从真实仓库解耦
  • 它让你可以精确控制依赖对象的行为
  • 它允许你验证调用是否发生了(行为验证)

总结一句话:

EXPECT_CALL(mock, hasInventory("item", 50)).WillOnce(Return(true));

意思是:

“我期望系统在运行过程中,调用 mock.hasInventory("item", 50) 方法一次,并返回 true。如果没发生,测试就失败。”

这是单元测试中模拟(mocking)依赖并验证行为的典型方式。

给出的代码和说明展示了如何使用 Mocking 框架(如 Google Mock 和 Trompeloeil)来模拟依赖行为并进行单元测试。下面是逐段解释和对比,帮助你深入理解:

背景情景:测试 Order 的行为

目标:测试 Order::fill() 方法在仓库没有库存的情况下能否正确设置状态为“未填充”。

使用 Fake 的方式(手写实现)

bool hasInventory(std::string const & s, int i) const {return false;
}
void OrderFillFromWarehouse(){Order order(TALISKER, 50);EmptyWarehouse warehouse{};  // 手写的 fake,始终返回 falseorder.fill(warehouse);ASSERT(not order.isFilled());
}

特点:

  • 你手动实现了一个 EmptyWarehouse 类来控制返回值
  • 用的是状态验证(检查 order 的状态);
  • 简单、易懂,但无法验证是否真的调用了 hasInventory() —— 即无法做行为验证

使用 GMock/GTest 实现行为验证:

TEST(OrderTest, EmptyWarehouse) {MockWarehouse warehouse{};Order order{TALISKER, 50};EXPECT_CALL(warehouse, hasInventory(TALISKER, 50)).WillOnce(Return(false));  // 设置行为期望order.fill(warehouse);ASSERT_FALSE(order.isFilled());
}

说明:

  • MockWarehouse 是用 Google Mock 定义的类;
  • EXPECT_CALL(...) 是行为验证:确保 Order 的代码中确实调用了 hasInventory(),且参数正确;
  • WillOnce(Return(false)) 是返回假的库存信息;
  • 更严谨的测试:能验证“怎么被调用”。

ON_CALL 的对比:

ON_CALL(warehouse, hasInventory(TALISKER, 50)).WillByDefault(Return(false));

区别:

  • ON_CALL 设置的是默认行为(默认返回值)
  • 如果没有 EXPECT_CALL,会发出运行时警告,提醒你没验证行为;
  • 建议搭配使用:用 ON_CALL 设置默认,用 EXPECT_CALL 做验证。

Trompeloeil 版本(C++ mock 框架):

void testMockingWithTrompeloeil(){MockWarehouse wh;//REQUIRE_CALL(wh, hasInventory(TALISKER, 50)).RETURN(false).TIMES(1);ALLOW_CALL(wh, hasInventory(TALISKER, 50)).RETURN(false);Order order(TALISKER, 50);order.fill(wh);ASSERT(not order.isFilled());
}

区别:

  • REQUIRE_CALL:严格行为验证(必须发生,参数必须匹配,调用次数也必须一致);
  • ALLOW_CALL:类似于 ON_CALL,用于放宽验证,仅设定行为;
  • Trompeloeil 语法清晰,更现代,常用于 C++20 项目;
  • 同样达到了 mock 的目的,但更易集成于现代 C++。

总结理解:

比较项Fake(手写类)GMock (EXPECT_CALL)Trompeloeil (REQUIRE_CALL)
控制依赖行为
行为验证(是否调用)
严格性可选(ON_CALL vs EXPECT_CALL)(REQUIRE_CALL)
易用性简单但功能弱强大,语法稍繁强大,现代语法

提供的这部分是关于 Behavior Verification(行为验证) 的实战案例,分别用 Google Mock 和 Trompeloeil 展示了“顺序期望”的使用,来验证代码是否以正确的顺序调用依赖对象(DOC)的方法。下面是详尽解析和理解。

什么是 Behavior Verification?

行为验证 = 确保 被测系统(SUT)依赖组件(DOC) 交互的方式符合期望

比如:

  • 调用了哪些方法?
  • 顺序是否正确?
  • 参数是否一致?
  • 调用了几次?

示例背景

你在测试 Order::fill() 方法,依赖一个 Warehouse

  • hasInventory():判断库存是否足够
  • remove():从库存中移除物品
    你希望测试:
  1. 是否调用了这两个方法?
  2. 顺序是否正确?(先查,再删)
  3. Order 最终是否被设置为已填充?

Google Mock 示例详解

TEST(OrderTest, FilledWarehouse)
{MockWarehouse warehouse{};InSequence s{}; // 顺序期望启用EXPECT_CALL(warehouse, hasInventory(TALISKER, 50)).WillOnce(Return(true));EXPECT_CALL(warehouse, remove(TALISKER, 50));Order order{TALISKER, 50};order.fill(warehouse);ASSERT_TRUE(order.isFilled());
}

关键点解释:

元素含义
InSequence s{}所有 EXPECT_CALL 的调用顺序必须严格匹配
EXPECT_CALL(...).WillOnce(...)设置期望调用 + 返回值
remove(...) 不设置返回值void 函数只验证调用本身
ASSERT_TRUE(order.isFilled())状态验证(结合行为验证)

Trompeloeil 示例详解

void testFulledOrderWithTrompeloeil(){MockWarehouse wh{};trompeloeil::sequence seq{};REQUIRE_CALL(wh, hasInventory(TALISKER, 50)).RETURN(true).TIMES(1).IN_SEQUENCE(seq);REQUIRE_CALL(wh, remove(TALISKER, 50)).TIMES(1).IN_SEQUENCE(seq);Order order(TALISKER, 50);order.fill(wh);ASSERT(order.isFilled());
}

Trompeloeil 写法对比:

TrompeloeilGoogle Mock
REQUIRE_CALL(...)EXPECT_CALL(...)
.IN_SEQUENCE(seq)InSequence s{} + 顺序写法
.RETURN(...).WillOnce(Return(...))
.TIMES(1)(默认一次)可选 .Times(1)

行为验证的挑战和副作用

Peter Sommerlad 强调的几点缺陷:

问题说明
DOC 不存在你必须先 mock 一个还没实现的组件,可能会限制设计自由
脆弱测试 Fragile如果 SUT 的内部调用顺序改变(但功能不变),测试就失败
过度规范 Over-specification规定太多细节,使得测试阻碍了重构
“硬测试”阻碍重构测试太依赖 SUT 的具体实现,难以随设计演进

小结

框架行为验证支持顺序验证接口模拟能力
GMockInSequenceMOCK_METHOD 系列
Trompeloeil.IN_SEQUENCE()REQUIRE_CALL

行为验证适合用于验证交互是否符合预期,但也要适度使用,避免影响设计和重构灵活性。

给出的内容是 Trompeloeil(一个现代 C++ mocking 框架)的速查表,主要用于帮助开发者快速写出模拟函数和行为验证。下面我将清晰地解释其设计理念和 DSL(领域特定语言)结构,帮助你全面理解。

问题背景:为何需要 DSL 指定行为?

  • Mock 对象中,我们要告诉它:
    • “什么样的调用是合法的?”
    • “什么情况下返回什么值?”
    • “是否调用过?调用了几次?”
    • “调用顺序如何?”
      为了灵活且可组合地描述这些行为,Mock 框架引入了小型 DSL,即你看到的 .RETURN(...), .WITH(...), .IN_SEQUENCE(...) 等语法。

Trompeloeil Mock 函数生成宏

用法说明
MAKE_MOCKn(name, sig)生成 非 const 成员函数 mock
MAKE_CONST_MOCKn(name, sig)生成 const 成员函数 mock
n 是参数个数,比如 MAKE_MOCK2(foo, void(int, double))

期望设置:REQUIRE / ALLOW / FORBID

说明
REQUIRE_CALL(obj, func(params))强制调用一次,否则测试失败
ALLOW_CALL(obj, func(params))允许调用,不会强制
FORBID_CALL(obj, func(params))禁止调用,若调用会失败
可加 NAMED_ 版本,用于命名和追踪期望对象(通过指针引用)
默认行为是:所有调用都是非法的,除非明确设定允许或期望。

附加条件 / 动作 DSL

语法说明
.WITH(cond)参数必须满足条件(参数只读)
.SIDE_EFFECT(stmt)每次调用执行一段副作用语句
.RETURN(expr)返回值(参数只读)
.THROW(expr)抛出异常(参数只读)
.LR_* 系列参数为 可变引用(可被修改)时使用

调用次数和顺序

语法说明
.TIMES(n)期望被调用 恰好 n 次
.TIMES(min, max)允许被调用 在 min 到 max 次之间
.AT_MOST(x) / .AT_LEAST(x)调用次数的便捷方式
.IN_SEQUENCE(seq)与其他期望按指定顺序匹配(顺序验证)
trompeloeil::sequence seq;
REQUIRE_CALL(obj, method1(_)).IN_SEQUENCE(seq);
REQUIRE_CALL(obj, method2(_)).IN_SEQUENCE(seq);

参数匹配器(Matchers)

通用匹配器:

匹配器意义
_任意值
eq(x)等于 x
ne(x)不等于 x
lt(x)小于 x
le(x)小于等于 x
gt(x)大于 x
ge(x)大于等于 x
re(x)正则匹配 /x/

示例:

REQUIRE_CALL(mock, func(eq("item"), ge(50)));

生命周期验证

你可以验证 Mock 对象是否被销毁:

auto obj = new deathwatched<MockWarehouse>();
REQUIRE_DESTRUCTION(*obj); // 测试通过仅在析构时调用

小结:Mock DSL 的语义结构

REQUIRE_CALL(mock, method(_)).WITH([](auto x) { return x > 0; })     // 参数过滤.RETURN(42)                             // 返回值.SIDE_EFFECT(counter++)                 // 副作用.IN_SEQUENCE(seq)                       // 顺序控制.TIMES(2);                              // 调用次数

这种结构清晰定义了:

  • 调用的合法性(是否允许)
  • 返回什么(行为)
  • 在什么条件下(参数)
  • 什么时候(顺序)
  • 多少次(频度)

关于使用 Mockator 进行简化 Mock 测试的方式,以及其背后的理念,结合 Kent Beck 和 Ward Cunningham 提出的 “Do the simplest thing that could possibly work” 原则。以下是对该内容的详细理解和解释。

理念背景:当你不知道怎么做时

“Do the simplest thing that could possibly work”

这是 Kent Beck 和 Ward Cunningham 在推动 极限编程(XP)测试驱动开发(TDD) 时提出的指导思想,意思是:

  • 先实现最小可行解法
  • 不要一开始就追求完美或泛化
  • 尤其在写测试或构建 Mock 时,不要引入不必要的复杂度。
    Mockator 就体现了这个理念:

Mockator 的简化 Mock 特性

Mockator 是一种模拟工具/框架,具有如下特点:

特性说明
无需大量 #define相比 Trompeloeil、GMock 等依赖宏定义,Mockator 更偏向常规 C++ 代码风格
使用 IDE(如 Cevelop)生成代码Seam 和调用跟踪的 Mock 代码由 IDE 自动生成
直接用 std::vector<call> 记录调用Mock 的行为跟踪通过 std::vector<call>
可用正则匹配调用使用 std::regex 实现灵活匹配

示例代码解析

MockWarehouse warehouse { };
OrderT<MockWarehouse> order(TALISKER, 50);
order.fill(warehouse);
ASSERT(order.isFilled());

此处使用了 模板参数化的 OrderT,将 Warehouse 替换成 MockWarehouse,引入了测试 seam。
然后调用跟踪如下:

calls expectedMockWarehouse{call("MockWarehouse()"),call("hasInventory(const std::string&, int) const", TALISKER,50),call("remove(const std::string&, int) const", TALISKER,50)
};
ASSERT_EQUAL(expectedMockWarehouse, allCalls[1]);

这里做了两件事:

  1. 定义了我们期望的调用序列(包括构造、调用的方法及参数)
  2. 断言实际调用与期望一致

优点总结

优点说明
简单直观Mock 实现靠真实 C++ 代码,减少宏和 DSL
易于生成借助 IDE 自动生成 Seam 和 Tracer
调试友好调用序列是清晰可比较的字符串列表
支持灵活匹配可以用 std::regex 灵活匹配复杂调用

总结

Mockator 是对“做最简单的事情”的真实实现。

它放弃了复杂的 DSL、宏系统,转而使用 模板参数化 + 手动或自动生成类 + 调用追踪 的组合,使得:

  • Mock 更像真实对象
  • 测试更可维护、更具可读性
  • 适合极限编程/迭代开发流程
    如果你倾向于简化 Mock 使用场景、希望避免框架引入的复杂性,Mockator 是很合适的选择。

这段内容是对 Mockator 框架中如何通过**调用顺序追踪(Sequencing)**进行 行为验证(Behavior Verification) 的一个详细展示。下面我来为你逐步讲解和整理这个流程,帮助你彻底理解。

背景知识:什么是行为验证 (Behavior Verification)

行为验证的核心目的是:

  • 验证系统在特定输入下,是否按照预期顺序与依赖对象(DOC)交互。
  • 不只关心结果(状态),还关心“怎么做的”。
    例如:
EXPECT_CALL(warehouse, hasInventory(...)).WillOnce(...);
EXPECT_CALL(warehouse, remove(...));

这种方式就属于行为验证 —— 你期望系统在调用 remove() 之前会先调用 hasInventory()

Mockator 实现思路概述

Mockator 和传统的 GMock/Trompeloeil 不同,它:

  • 使用 vector<call> 存储方法调用记录
  • 利用 mock_id 区分每个 mock 实例
  • 通过 全局 allCalls 向量 实现每个 mock 对象调用顺序的追踪
  • 期望值定义为 calls expectedMockWarehouse{...},直接使用 std::string 对方法名称+参数的匹配

代码逐段解析

初始化追踪系统

INIT_MOCKATOR();
static std::vector<calls> allCalls(1);
  • INIT_MOCKATOR():初始化调用追踪系统(宏,设置状态)
  • allCalls(1):只追踪一个 mock 对象的调用

定义 MockWarehouse

struct MockWarehouse {size_t const mock_id;MockWarehouse(): mock_id(reserveNextCallId(allCalls)) {allCalls[mock_id].push_back(call("MockWarehouse()"));}bool hasInventory(std::string const& what, int howmany) const {allCalls[mock_id].push_back(call("hasInventory(const std::string&, int) const", what, howmany));return bool(); // 默认返回 false}void remove(std::string const& what, int howmany) const {allCalls[mock_id].push_back(call("remove(const std::string&, int) const", what, howmany));}
};

解释:

  • mock_id:为每个 mock 对象分配唯一 ID(用于在 allCalls 中索引)
  • 所有方法都记录了自身的调用,包括名称和参数(作为字符串)
  • Cevelop IDE 会自动生成这段代码,你只需写业务测试逻辑

编写测试

MockWarehouse warehouse{};
OrderT<MockWarehouse> order(TALISKER, 50);
order.fill(warehouse);
ASSERT(not order.isFilled());

这部分是测试业务逻辑 —— 下单时库存为空,订单不应被填充。

定义并验证期望调用序列

calls expectedMockWarehouse{call("MockWarehouse()"),call("hasInventory(const std::string&, int) const", TALISKER, 50),call("remove(const std::string&, int) const", TALISKER, 50)
};
ASSERT_EQUAL(expectedMockWarehouse, allCalls[1]);

这里重点来了:

  • 你定义了期望的调用序列
  • expectedMockWarehouse 是对 mock 的期望行为(包括调用的顺序)
  • allCalls[1] 是实际发生的调用记录(注意索引1是分配给 mock 的)
    通过 ASSERT_EQUAL 来断言行为和顺序都一致。

核心理解总结

解释
Mock 对象自动生成Mockator 借助 IDE(Cevelop)生成标准 C++ 类
方法调用记录为字符串使用 std::string 表示每个调用及参数
支持顺序验证通过 vector 顺序匹配实际调用与期望调用
非侵入式不依赖宏或 DSL,代码更干净
简化行为验证无需复杂 EXPECT_CALL,只需构造 expected calls

补充可视化建议

如果你需要我画一个对应的调用序列图或类图来帮助你可视化整个过程(如 Order.fill(...) 调用了哪些 mock 方法、以什么顺序),我可以使用 Mermaid 来实现它。你需要吗?
例如:

UnitTest Order MockWarehouse fill(MockWarehouse) hasInventory("TALISKER", 50) remove("TALISKER", 50) isFilled() UnitTest Order MockWarehouse

这段话总结了Mocking Frameworks 的设计起源和特点,特别是 Java 生态中的 Mock 框架,比如早期的 JMock、EasyMock 以及 Google 的 GMock。下面是详细理解:

1. 设计起源

  • 很多现代 Mock 框架设计理念,源自 JMock 和 EasyMock
    这两者是比较早的 Java Mock 框架,后来 GMock(Google Mock,C++ Mock 框架)在设计上受到影响。

2. Java Mock 框架特点

  • 基于反射(Reflection)实现
    通过 Java 的反射机制动态创建 Mock 对象,动态调用方法。
  • 只能 Mock 类和对象,依赖动态多态(动态绑定)
    Java 中 Mock 依赖继承、接口实现、动态派发方法。
  • 没有 Lambda 表达式(老版本)
    早期版本没有 lambda,行为匹配和定义只能依赖反射及接口方法签名。
  • 通过反射生成子类并重写方法实现 Mock 行为
    框架会生成 Mock 类的子类,在调用方法时注入自定义逻辑。

3. 行为定义方式

  • 使用 DSL(领域专用语言)描述行为,而非直接写普通代码
    例如 JMock、EasyMock、GMock 都用类似的 Expectation/ExpectationSet DSL 来描述期望行为,调用顺序,参数匹配等。

4. 总结理解

  • Mock 框架的核心是动态替换真实对象的行为,用于测试隔离。
  • Java 里主要通过反射和动态代理实现,不依赖语言特性(lambda)。
  • DSL 帮助测试者用简洁的语言声明 Mock 期望,方便行为验证。

总结了 C++ Mocking Frameworks(C++模拟测试框架)面临的典型问题,尤其是它们受限于C++语言特性,以及在设计上往往照搬Java框架的模式带来的不足。下面是详细理解:

C++ Mocking Frameworks 常见问题解析

1. 设计模仿 Java Mock 框架,缺乏利用 C++ 语言优势

  • 许多C++模拟框架设计思路直接跟随了 JMock / EasyMock,这两者是基于 Java 的设计模式。
  • 但 C++ 语言有其独特特性(如模板、强类型、无反射),简单套用 Java 方案会丢失 C++ 的优势。

2. 缺乏有用的反射机制

  • Java 有强大的运行时反射,而 C++ 标准并没有(直到最近的标准才开始引入有限反射)。
  • 因此 C++ 只能借助预处理宏(Macros)来辅助生成函数名、模拟函数定义。

3. 依赖子类化和虚函数,有时需要底层黑魔法

  • C++ Mocking 主要通过继承虚函数类并重写函数实现 Mock 行为。
  • 某些框架甚至使用未定义行为或者操作底层ABI(比如替换虚表vtable指针),来绕过限制(例:Hippomocks)。
  • 这种做法存在移植性和稳定性风险。

4. 使用 DSL(基于宏的魔法)定义期望行为

  • 行为定义用类似 Expectation 这样的 DSL(宏扩展),而非直接写纯 C++ 代码。
  • 例如 EXPECT_CALL 宏链式调用设置调用次数、返回值等。

5. 行为匹配隐式发生在对象析构函数时

  • 框架通常会在 Mock 对象析构时自动检查实际调用是否符合预期。
  • 这种隐式校验对调试和理解测试流程带来一定难度。

6. 示例:EXPECT_CALL 宏的典型用法

EXPECT_CALL(turtle, GetY()).WillOnce(Return(100)).WillOnce(Return(200)).WillRepeatedly(Return(300));
  • 表示对 GetY() 的调用,第一次返回100,第二次返回200,其后一直返回300。

7. 缺点总结

  • fragile(脆弱):代码重构时 Mock 相关测试容易失效。
  • 难复用:Mock 设置难以在不同测试间共享。
  • 测试臃肿:测试代码膨胀,难维护。

总结

  • C++ Mock 框架仍然被语言自身的限制和设计模式影响,面临灵活性和稳定性的挑战。
  • 目前多依赖宏和继承虚函数模拟行为,带来不少副作用和复杂性。
  • 随着C++反射等语言特性的逐步完善,未来 Mock 框架可能会更优雅。

这段内容主要讲了**“过度使用 Mock 的问题”**,尤其是它对测试设计和代码质量的负面影响。以下是详细理解:

Too much Mocking(过度 Mocking)问题点解析

1. 白盒测试导致测试代码、被测系统(SUT)和依赖对象(DOC)紧密耦合

  • 测试依赖于内部实现细节,导致三者绑定过紧。
  • 代码稍作修改就可能导致测试失败,难以维护。

2. 重构困难,设计灵活性丧失

  • 测试代码和生产代码强耦合,重构工具难用,人工维护成本高。
  • 设计灵活度受限,难以演进。

3. 促进状态化接口设计(Stateful APIs)

  • 当使用 Mock 来测试 SUT 和 DOC 的交互时,常见设计会逐渐变成带状态的接口。
  • 例如,Mock 对象暴露类似 setWiggle(Wiggle), setWaggle(Waggle), WiggleTheWaggle() 这类状态修改和操作接口。
  • SUT 依赖调用它们的顺序,形成时序耦合(temporal coupling)

4. 单参数或无参数函数和调用顺序依赖的趋势

  • Mock 设计中容易出现无参数函数(niladic)或者单参数函数。
  • 函数调用必须按特定顺序执行,增加测试复杂性。

5. Uncle Bob 的“Clean Code”被误解

  • Uncle Bob 其实强调避免“时间耦合”,即避免代码依赖调用顺序。
  • 但很多人误解为“鼓励无参数函数(niladic functions)”,反而导致更多时序依赖。

总结

  • 过度 Mocking 会让测试变得脆弱且难以维护。
  • 白盒测试如果过度关注细节,会丧失设计弹性和重构能力。
  • 设计应避免时序依赖和状态化接口,保持模块独立和行为简单。
  • 应正确理解“避免时间耦合”的意义,不是简单无参数函数,而是避免调用顺序带来的耦合。

你这段内容主要在说配置型测试替身(configurable test double)的问题,特别是使用类似 EXPECT_CALL 这种 DSL(领域专用语言)时带来的复杂性和混乱。这里我帮你梳理和总结理解,并用 Mermaid 逻辑图表现流程。

理解

问题核心

  • DSL(如 EXPECT_CALL)用于配置 Mock 行为,写法复杂且混合了行为和期望,导致测试代码难以理解和维护。
  • 例如:
    EXPECT_CALL(turtle, GetY()).WillOnce(Return(100)).WillOnce(Return(200)).WillRepeatedly(Return(300));
    
    这段代码混合了调用期望和返回值配置,写起来繁琐。
  • 可能伴随 ON_CALL 警告和隐式检查,增加调试难度。

测试替身(Test Double)的生命周期涉及的阶段:

  • Setup (准备测试环境)
  • Configuration (配置 Mock 行为和期望)
  • Installation (装配 Mock 进 SUT)
  • Exercise (执行 SUT 代码)
  • Return Values (根据配置返回数据)
  • Verify (校验期望是否达成)
  • Teardown (清理)

Mermaid 逻辑图示例

Setup
Configuration
Installation
Exercise
ReturnValues
Verify
Teardown

各节点说明:

  • Setup:准备测试环境,初始化对象
  • Configuration:用 EXPECT_CALL 或 ON_CALL 设定 Mock 的行为和期望
  • Installation:将 Mock 关联到被测系统(SUT)
  • Exercise:运行测试操作,驱动 SUT 调用 Mock
  • ReturnValues:Mock 根据配置返回指定数据
  • Verify:检查调用是否符合预期(如调用次数、顺序、参数等)
  • Teardown:清理测试环境,释放资源

你可以这样理解

  • 复杂 DSL 语法把行为和期望混在一起,使测试脚本难维护。
  • 测试替身配置是整个测试流程中间关键且复杂的一环。
  • 这个模型帮助拆解和理清测试替身的使用步骤。

这部分强调了**紧耦合(tight coupling)**带来的风险:

  • 测试代码和被测代码(SUT)之间耦合过紧,会导致:
    • 重构困难:改动 SUT 代码可能导致大量测试失败,维护成本变高。
    • 测试脆弱:测试对内部实现细节依赖太多,稍微变动接口或行为就可能破坏测试。
    • 这就是所谓的“GLUE”——测试代码和生产代码之间过度粘合,失去灵活性。
      可以用一句话总结:

过度 Mock 导致测试、SUT 和依赖对象(DOC)之间的紧耦合,使得重构成本变高,测试变得脆弱。

如果用Mermaid图来表达“紧耦合”的危害,可能是这样:

tight coupling
tight coupling
fragile dependency
Test
SUT
DOC

Stateful APIs are bad?!

loop
acquire
use
release

这部分讲的是有状态API(Stateful APIs)的问题

核心点:

  • 有状态API指的是必须按照特定顺序调用多个方法,维护内部状态的API。比如图示的流程:
    • acquireuserelease,并且 use 可以循环多次。
  • **RAII(资源获取即初始化)**模式是OK的,因为资源的获取和释放是自动且绑定生命周期的,比如C++的智能指针:
    • 只要对象生命周期结束,资源自动释放,避免忘记释放。
  • 如果没有RAII,状态管理就是“坏”的:
    • 用户可能会忘记调用释放,导致资源泄漏。
    • 复杂的状态机使得接口难以正确使用,也难以测试。
  • 典型有状态API示例:
    • 文件操作:openread/writeclose
    • 网络操作:socketbindlistenacceptclose

结论:

  • 简单的状态转换且自动管理(如RAII)是可以接受的。
  • 复杂且需要显式管理状态的API容易出错且难以测试。

这段内容强调了Bad Stateful APIs 的具体案例和现实中的复杂性,重点如下:

Bad Stateful APIs:套接字(Sockets)

  • 传统Unix资源API(文件)是单步初始化 - 操作 - 关闭,结构简单。
  • BSD套接字API很复杂,必须进行多阶段初始化:
    • 例如:socketbindlistenacceptread/writeclose
    • 或者:socketbind(可选)→ connectread/writeclose
  • 多语言中大量包装库仍保留这种多步骤初始化方式。
  • 建议:自己写库时不要这样设计,多步骤状态管理难用且易错!

Stateful APIs 是真坏吗?

  • 许多图形库也是状态驱动的(如Turtle图形,Cairo库)。
  • 它们通常用一长串“setter”调用,最后执行动作。
  • 多步初始化常常未封装,导致使用复杂。
  • 小改动可能引发大问题,代码难理解。
  • 好在通常有合理的默认值缓解了部分问题。

例子:Cairo 图形库代码片段

cairo_text_extents_t te;
cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
cairo_select_font_face(cr, "Georgia",CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
cairo_set_font_size(cr, 1.2);
cairo_text_extents(cr, "a", &te);
cairo_move_to(cr, 0.5 - te.width / 2 - te.x_bearing,0.5 - te.height / 2 - te.y_bearing);
cairo_show_text(cr, "a");
  • 这里可以看到很多set操作设置状态,最后才有绘制动作。

总结:

  • 多步状态ful API 确实难用且难测,尤其当多阶段初始化没封装时。
  • 但是在某些领域(图形库)这种风格广泛存在,合理默认值和封装能减轻痛苦。
  • 设计时应尽量简化状态管理,减少多步初始化,或封装成单步调用。

这段内容围绕序列化调用和什么时候用序列匹配器(sequence matchers),还有针对Legacy代码和Mocking的总结,重点如下:

需要序列化调用的场景(Sequencing)

  • 代码示例:
    last(third(second(first(something))));
    
  • 通过将前一步的结果传给下一步实现序列调用
  • 注意C++中函数参数的未定义顺序求值,可能导致意外问题
  • 封装逻辑序列
    • 利用类构造函数(ctors)
    • 命名函数封装序列
  • 设计权衡:
    • 泛化和灵活性 vs. 可理解性和维护性
  • 小心“编程巧合”(Programming by coincidence)

什么时候使用序列匹配器(sequence matchers)

  • 当你面临有状态且依赖调用顺序的、不能改动的第三方API/DOC
  • 不能用无状态的Facade包装它
  • 例如:正在构建对这个API的包装器
  • 也可能是设计模式,比如Builder模式

总结

  • 测试遗留代码时,最好先引入Seams(测试接口)和Stub DOC
  • 利用C++和IDE的力量简化测试代码
  • 只有在必须测试无法改变的有状态API时才用Mocks
  • 警惕mock框架给新代码设计带来的坏影响
  • 不要用mock框架模拟还没写好的代码(防止过早设计)
  • 记住KISS原则(Keep It Simple, Stupid)也适用于自动化测试

相关文章:

CppCon 2017 学习:Mocking Frameworks Considered

当然可以&#xff0c;下面是对 Fowler 的 Whiskey-Store 示例。 Fowler 的 Whiskey-Store 示例&#xff08;坏设计&#xff09; 贴出的类图是 Martin Fowler 在《重构》书中使用的一个教学用反面案例&#xff08;故意设计得不合理&#xff09;&#xff0c;用来说明如何通过重…...

通过事件过滤器拦截QRadioButton点击事件

通过事件过滤器拦截QRadioButton点击事件 一、事件过滤器完整实现 1. 核心代码扩展&#xff08;含注释&#xff09; bool MainWindow::eventFilter(QObject* obj, QEvent* ev) {// 拦截所有QRadioButton的鼠标事件&#xff08;包括点击、释放、双击&#xff09;if (ev->ty…...

领码 SPARK 融合平台赋能工程建设行业物资管理革新——数智赋能,重塑中国模式新范式

摘要 工程建设行业正加速迈向数字化与精益化转型&#xff0c;物资管理成为项目成败的关键瓶颈。本文深入解析中国工程企业“项目部-物资部-企业项目管理部”三级协同的独特物资管理体系&#xff0c;聚焦集中采购与零星采购的统筹难题。基于领码 SPARK 融合平台&#xff0c;提出…...

“地标界爱马仕”再启:世酒中菜联袂陈汇堂共筑新会陈皮顶奢产业

“地标界爱马仕”再启战略新篇&#xff1a;世酒中菜联袂陈汇堂&#xff0c;共筑新会陈皮顶奢产业生态 ——中世国际与陈汇堂股权合作签约仪式在国际地理标志服务基地举行 江门市新会区&#xff0c;2025年6月20日——被誉为“地标界爱马仕”的全球顶奢品牌运营商世酒中菜 &…...

.Net Framework 4/C# 数据访问技术(ADO.NET)

一、数据库基础 (一) 数据库简介 数据库是按照数据结构来组织、存储和管理数据的仓库,是存储在一起的相关数据的集合。 (二) SQL 语言简介 SQL 是一种数据库查询和程序设计语言,用于存取数据以及查询,更新和管理关系型数据库系统。在编写 SQL 语句时,SQL 语句各关键字要以…...

北京京东,看看难度

最近由于三大外卖平台“打仗”&#xff0c;优惠券多到数不过来&#xff0c;一日三餐每个平台各点一单哈哈哈&#xff0c;正好最近组织内部还有朋友在北京的京东面试过&#xff0c;分享一下她的面经&#xff08;Java岗&#xff09;&#xff1a; 1. Kafka消息不丢失问题&#xf…...

RPGMZ游戏引擎 如何手动控制文字显示速度

直接上代码 const _Window_Base_prototype_initialize Window_Base.prototype.initialize;Window_Base.prototype.initialize function(rect) {_Window_Base_prototype_initialize.call(this, rect);this.文字速度缓冲 0;}; this.文字速度缓冲 0; 进行缓冲 Window_Base…...

linux线程同步

互斥锁 同步与互斥概述** 现代操作系统基本都是多任务操作系统&#xff0c;即同时有大量可调度实体在运行。在多任务操作系统中&#xff0c;同时运行的多个任务可能&#xff1a; 都需要访问/使用同一种资源 多个任务之间有依赖关系&#xff0c;某个任务的运行依赖于另一个任…...

大内存对电脑性能有哪些提升

在科技飞速发展的今天&#xff0c;电脑已经成为我们生活和工作中不可或缺的伙伴。无论是日常办公、追剧娱乐&#xff0c;还是进行复杂的游戏和专业设计&#xff0c;电脑的性能都至关重要。而在影响电脑性能的众多因素中&#xff0c;内存大小常常被人们忽视。 多任务处理更流畅…...

什么是“微博养铁粉”以及如何增加微博铁粉

发了个发微博养铁工具_微博养铁粉的定义 微博养铁粉是指粉丝通过与博主的互动&#xff0c;成为博主的铁粉。铁粉是微博推出的一种反映粉丝与博主之间亲密度的互动产品。成为铁粉后&#xff0c;粉丝的评论权重增加&#xff0c;更容易上前排&#xff0c;点赞和评论的效果也会更好…...

华为和H3C服务器配置远控管理地址

1、华为RH2288_V3服务器 1.1、启动服务器按DEL按键进入服务器bios 1.2、选择Advanced菜单中的 IPMI iBMC Configuration配置项回车进入。 1.3、IPMI iBMC Configuration配置界面中选择IBMC Configuration配置项回车进入。 1.4、IBMC Configuration 配置项中配置IPV4 Configura…...

Git 查询与切换分支的完整指南

Git 查询与切换分支的完整指南 1. 查询分支列表 查看本地分支 git branch当前分支会以绿色显示并带有 * 标记添加 -v 或 -vv 查看更详细的信息&#xff08;最后一次提交和跟踪关系&#xff09; git branch -v # 或者 git branch -vv查看所有分支&#xff08;包括远程分支&a…...

Spring 中的依赖注入(DI)详解

&#x1f4cc; 摘要 在现代 Java 开发中&#xff0c;依赖注入&#xff08;Dependency Injection, DI&#xff09; 是 Spring 框架最核心的功能之一。它通过解耦对象之间的依赖关系&#xff0c;提高了代码的可维护性、可测试性和可扩展性。 本文将全面讲解 Spring 中依赖注入的…...

Bytebase 3.7.1 - 数据库变更功能全免费!

&#x1f514; 重大变更 所有数据库变更相关功能现已在社区版中完全免费开放&#xff01;详情请查看我们的最新定价。 &#x1f384; 改进 文档网站全面升级&#xff0c;改进导航、搜索功能&#xff0c;以及与 AI 集成自助回答问题。SQL 编辑器现在会高亮光标所在的语句。SQ…...

深度学习笔记27-LSTM实现糖尿病探索与预测(Pytorch)

&#x1f368; 本文为&#x1f517;365天深度学习训练营中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 一、前期准备 1.数据导入 import torch.nn as nn import torch.nn.functional as F import torchvision,torch import numpy as np import pandas as pd impo…...

3DS中文游戏全集下载 任天堂3DS简介3DS第一方独占游戏推荐

任天堂3DS 的详细介绍&#xff0c;涵盖其硬件特性、核心功能、游戏阵容及历史地位&#xff1a; 3DS游戏全集下载 https://pan.quark.cn/s/dd40e47387e7 https://sink-698.pages.dev/3ds CIA CCA 等格式可用于3DS模拟器和3DS实体机 3DS 是什么&#xff1f; 全称&#xff1a;Nin…...

vue3 reactive重新赋值

在 Vue 3 中&#xff0c;如果你想使用 reactive API 来创建一个响应式对象&#xff0c;并且之后需要更新这个对象中的属性&#xff0c;你可以按照以下步骤进行&#xff1a; 1. 使用 reactive 创建响应式对象 首先&#xff0c;你需要从 Vue 的 reactive API 中创建一个响应式对…...

全面掌握 C++ 基础:关键特性与进化

文章目录 全面掌握 C 基础&#xff1a;关键特性与进化1. C 关键字2. 命名空间&#xff08;namespace&#xff09;⚠️ 示例 2.1 定义命名空间2.2 使用成员的方法 3. C 输入/输出&#xff08;iostream&#xff09;4. 缺省参数&#xff08;Default Parameter&#xff09;4.1 定义…...

HTML一键打包EXE串口API介绍

HTML一键打包EXE软件(HTML转EXE) 支持将Web前端项目转换为Windows平台下的独立可执行程序&#xff08;EXE&#xff09;&#xff0c;适用于Windows 7及以上系统&#xff0c;无需额外配置系统环境, 软件包含多种内核, 包括IE内核, Chrome内核, 以及WebView2(永久免费), 适用于不同…...

.docx 和 .doc 都是 Word 文档格式的区别

.docx 和 .doc 都是 Word 文档格式&#xff0c;但有区别&#xff1a; .docx 是新版 Word 格式&#xff08;推荐使用&#xff09; 从 Microsoft Word 2007 起引入的格式全名是&#xff1a;Office Open XML Document实际是一个 压缩包&#xff08;ZIP&#xff09;结构&#xff0…...

如何轻松地将音乐从 iPhone 传输到 Mac?

想把音乐从 iPhone 传输到 Mac 吗&#xff1f;这很常见&#xff0c;无论你是想更换设备、备份收藏&#xff0c;还是只想在更大的屏幕上欣赏喜爱的歌曲。幸运的是&#xff0c;有 6 种有效的方法可以完成这项工作&#xff0c;具体取决于你喜欢使用的工具。让我们开始吧。 第 1 部…...

Qwen3 Embedding 结构-加载-训练 看透模型设计哲学

看透一个顶级AI句向量模型的设计秘密&#xff0c;从文件结构到加载原理&#xff0c;再到其背后的训练哲学。 1 Qwen3-Embedding模型结构拆解 说明&#xff1a;目录包含了运行一个基于 Transformer 的句向量模型所需的所有组件 文件类别核心文件作用核心模型model.safetensors…...

AT8548双通道 H 桥电机驱动芯片

AT8548 是一种双通道低饱和电压的正、反向电机驱动芯片&#xff0c;为玩具、打印机和其它电机一体化应用提供一种双通道电机驱动方案。 特点&#xff1a; 双通道H桥电机驱动器&#xff1b; 驱动两个直流有刷电机或者一个步进电机&#xff1b; 低RDS(ON)电阻&#xff0c;1.06Ω(…...

kubeadm worker节点加入master失败

文章目录 1、操作2、问题现象3、问题原因4、问题解决4.1、重新生成token4.2、重新生成hash值 5、验证 1、操作 执行以下命令&#xff0c;让worker节点加入到master节点 kubeadm join 103.123.222.241:6443 --token vxe3v1.wzpnks8v1vbbtsu0 --discovery-token-ca-cert-hash s…...

Maven 之工程化开发核心指南:插件配置、pom 文件与依赖管理

目录 1. &#x1f9e9;Maven插件 2. &#x1f3d7;️构建Maven工程 3. &#x1f4c4; pom文件配置 ​3.1. ⚙️ ​ 中定义一些配置信息 ​3.2. &#x1f4e6; ​中定义依赖的jar包坐标 ​3.3. &#x1f50c; ​中定义第三方插件 ​4. ✍️编写代码 5. &#x1f517;依赖范…...

分布式系统中的 Kafka:流量削峰与异步解耦(二)

Kafka 在分布式系统中的应用案例 电商订单系统 在电商领域&#xff0c;订单系统是核心业务模块之一&#xff0c;涉及多个复杂的业务环节和系统组件之间的交互。以常见的电商购物流程为例&#xff0c;当用户在电商平台上下单后&#xff0c;订单创建服务会首先接收到用户的订单…...

从服务器收到预料之外的响应。此文件可能已被成功上传。请检查媒体库或刷新本页

如果php.ini已经加入了如下的内容还是报错 &#xff1a; upload_max_filesize 1024M post_max_size 1024M 那就是因为阿帕奇导致&#xff1a;...

FramePack 安装指南(中文)

FramePack 安装指南&#xff08;中文&#xff09; -Windows FramePack 是最前沿的 AI 视频生成框架&#xff0c;以极小的硬件需求颠覆视频创作&#xff01;它能在仅 6GB 笔记本 GPU 内存上&#xff0c;驱动 13B 模型以 30 FPS 生成超长 120 秒视频&#xff0c;几乎无内容限制&…...

【NLP入门系列三】NLP文本嵌入(以Embedding和EmbeddingBag为例)

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 博主简介&#xff1a;努力学习的22级本科生一枚 &#x1f31f;​&#xff1b;探索AI算法&#xff0c;C&#xff0c;go语言的世界&#xff1b;在迷茫中寻找光芒…...

电子电气架构 --- 软件供应商如何进入OEM体系

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 简单,单纯,喜欢独处,独来独往,不易合同频过着接地气的生活,除了生存温饱问题之外,没有什么过多的欲望,表面看起来很高冷,内心热情,如果你身…...

检索增强生成(RAG)领域关键数据集综述:分类、挑战与展望

检索增强生成&#xff08;RAG&#xff09;领域关键数据集综述&#xff1a;分类、挑战与展望 摘要 检索增强生成&#xff08;RAG&#xff09;通过融合外部知识库与大型语言模型&#xff0c;已成为解决知识密集型自然语言处理&#xff08;NLP&#xff09;任务的关键范式。高质量…...

CFD仿真计算革命:基于GPU的格子玻尔兹曼方法(LBM)算子优化——利用Tensor Core加速碰撞核计算(性能提升3倍实测)

点击 “AladdinEdu&#xff0c;同学们用得起的【H卡】算力平台&#xff0c;注册即送H800算力”&#xff0c;H卡级别算力&#xff0c;按量计费&#xff0c;灵活弹性&#xff0c;顶级配置&#xff0c;学生专属优惠。 在计算流体动力学领域&#xff0c;格子玻尔兹曼方法正以介观模…...

【蓝牙】Qt4中向已配对的手机发送PDF文件

在Qt 4中实现通过蓝牙向已配对的设备发送文件&#xff08;例如PDF文件&#xff09;&#xff0c;你可以使用Qt Bluetooth模块。Qt 4的蓝牙模块提供了基本的蓝牙功能&#xff0c;包括设备发现、配对管理和数据传输。下面是一些步骤和示例代码&#xff0c;帮助你通过蓝牙发送PDF文…...

Vue + AbortController 请求取消弹窗 hook 封装

背景 实际业务开发场景中&#xff0c;往往存在有些大数据请求的需求&#xff0c;一旦请求发起加载遮罩后用户就无法操作了&#xff0c;直接尬住&#xff0c;所以提供一个支持取消查询的功能还是很有必要的&#xff0c;为了在全业务接口都能使用封装一个hook。 ✋为什么要用 A…...

在小程序中实现上下左右拖动表格

在小程序的开发中&#xff0c;不可避免会出现上下左右拖动表格的类似需求&#xff0c;下面将把这个简单实现一下 其中主要使用到了overflow: scroll;来使得横向和纵向可以滚动&#xff0c;并且使用负边距 父容器截断的方法来同时隐藏横向和纵向滚动条&#xff0c;从而实现该效…...

Spark 以及 spark streaming 核心原理及实践

导语 spark 已经成为广告、报表以及推荐系统等大数据计算场景中首选系统&#xff0c;因效率高&#xff0c;易用以及通用性越来越得到大家的青睐&#xff0c;我自己最近半年在接触spark以及spark streaming之后&#xff0c;对spark技术的使用有一些自己的经验积累以及心得体会&…...

数据融合平台是什么?如何搭建数据融合平台?

目录 一、数据融合是什么 1. 定义 2. 作用 二、数据融合平台的功能是什么 1. 数据抽取 2. 数据清洗 3. 数据转换 4. 数据关联 5. 数据存储 三、如何让搭建数据融合平台 1. 需求分析 2. 选择合适的技术和工具 3. 设计平台架构 4. 开发和部署平台 5. 数据迁移和融…...

Linux之线程同步与互斥

目录 一、线程互斥 1.1、进程线程间的互斥相关背景概念 1.2、互斥量mutex 1.2.1、互斥量的接⼝ 1.3、互斥量实现原理探究 1.4、互斥量的封装 二、线程同步 2.1、条件变量 2.2、同步概念与竞态条件 2.3、条件变量函数 2.4、⽣产者消费者模型 2.4.1、为何要使⽤⽣产者…...

uniapp开发小程序,导出文件打开并保存,实现过程downloadFile下载,openDocument打开

uniapp开发小程序&#xff0c;导出文件打开并保存 实现思路 1、调用请求获取到后端接口返回的下载文件的url路径 &#xff08;注意必须是https的路径&#xff0c;域名需要配置在微信小程序后台的合法域名里面&#xff09; 2、使用 uni.downloadFile 方法 &#xff08;下载文件…...

腾讯云COS“私有桶”下,App如何安全获得音频调用流程

流程图 #mermaid-svg-Phy4VCltBRZ90UH8 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-Phy4VCltBRZ90UH8 .error-icon{fill:#552222;}#mermaid-svg-Phy4VCltBRZ90UH8 .error-text{fill:#552222;stroke:#552222;}#me…...

简单的 ​Flask​ 后端应用

from flask import Flask, request, jsonify, session import os app Flask(__name__) app.secret_key os.urandom(24) users { 123: admin, admin: admin } # 登录接口 app.route(/login, methods[POST]) def login(): data request.get_json() username data.get(usern…...

Android 中 解析 XML 字符串的几种方式

在 Android 开发中&#xff0c;解析 XML 文件有多种方式&#xff0c;每种方式都有其特点和适用场景。常见的 XML 解析方式有 DOM 解析、SAX 解析 和 XmlPullParser 解析。 1、DOM 解析 DOM&#xff08;Document Object Model&#xff09;解析是一种基于树结构的解析方式&#…...

git commit

‌git commit 是版本控制的核心操作之一&#xff0c;用于将暂存区的修改记录为新的版本提交‌。以下是关键步骤和最佳实践&#xff1a; ‌基础操作‌ ‌提交单个文件‌&#xff1a; bash Copy Code git commit -m “提交信息” ‌提交多个文件‌&#xff1a; bash Copy Code …...

【新手向】GitHub Desktop 的使用说明(含 GitHub Desktop 和 Git 的功能对比)

GitHub Desktop 是 GitHub 公司推出的一款桌面应用程序&#xff0c;旨在帮助开发人员更轻松地使用 GitHub&#xff0c;以下是其简单的使用说明&#xff1a; 安装与登录 下载 GitHub Desktop |GitHub 桌面 访问GitHub Desktop 官方网站&#xff0c;根据自己的操作系统下载对应的…...

Tomcat项目本地部署(Servlet为例)

在Windows上部署 在idea中打开项目 首先我们需要准备一个Servlet项目&#xff0c;我之前的Servlet项目是用eclipse写的&#xff0c;这种情况下如果用idea直接打开的话会出现左侧目录无法显示的情况&#xff0c;这个时候我们就需要用别的方法打开 打开项目管理 如下图&#…...

Linux——linux的基本命令

目录 一、linux的目录结构 二、绝对路径和相对路径 三、文件类型&#xff08;linux下所有东西都可看作文件&#xff09; 四、文件的权限 五、文件权限的修改&#xff08;chmod&#xff09; 六、linux常用的命令 七、文件查看命令 八、文件编辑命令 九、文件压缩与解压…...

wireshark过滤显示rtmp协议

wireshark中抓包显示的数据报文中&#xff0c;明明可以看到有 rtmp 协议的报文&#xff0c;但是过滤的时候却显示一条都没有 查看选项中的配置&#xff0c;已经没有 RTMP 这个协议了&#xff0c;已经被 RTMPT 替换了&#xff0c;过滤框中输入 rtmpt 过滤即可...

Fiddler抓包工具使用技巧:如何结合Charles和Wireshark提升开发调试效率

在开发过程中&#xff0c;网络调试工具是每个程序员的必备利器&#xff0c;特别是当涉及到Web应用和移动应用的调试时&#xff0c;抓包工具的作用尤为突出。无论是处理复杂的API调用、分析性能瓶颈&#xff0c;还是排查网络通信问题&#xff0c;抓包工具都能够帮助开发者精准地…...

LVS负载均衡群集

这里写目录标题 案例:部署Tomcat案例分析案例概述案例前置知识点Tomcat 简介应用场景 案例环境 案例实施实施准备关闭 firewalld 防火墙在安装Tomcat之前必须先安装JDK 查看JDK是否安装安装配置 TomcatTomcat 的安装和配置步骤如下:解压后生成 apache-tomcat-9.0.8文件夹&#…...

【unitrix】 3.5 类型级别的比较系统(cmp.rs)

一、源码 这段代码定义了一个类型级别的比较系统&#xff0c;主要用于在编译时比较类型并得出比较结果。它使用了 Rust 的类型系统和标记特征(trait)来实现这一功能。 use crate::sealed::Sealed; use crate::number::{Z0, P1, N1}; use core::cmp::Ordering;// 比较结果类型…...