CPP-Summit-2022 学习:C++开发者测试最佳实践 (续)
测试对象应尽量选择模块而非单个类函数。接口和依赖注入(DI)是提高测试可控性的重要手段。UML 类图可以清晰描述模块依赖和接口实现,为测试设计提供参考。分层图(Mermaid Graph)→ 描述系统结构序列图(Mermaid SequenceDiagram)→ 描述 TLS 握手流程设计思想分层解耦接口可替换数据驱动测试C++实践每层模块化,依赖注入可用 Mock 替换真实实现提高可测性和可靠性
·
完整的火星车方向转向例子,数据用 .param 文件分离。
工程结构
project/
├── CMakeLists.txt
├── src/
│ └── orientation.h
└── test/
├── orientation_test.cpp
└── orientation.param
1. orientation.h
#pragma once
class Orientation {
public:
static Orientation N() { return Orientation('N'); }
static Orientation E() { return Orientation('E'); }
static Orientation S() { return Orientation('S'); }
static Orientation W() { return Orientation('W'); }
Orientation turnLeft() const {
switch (dir_) {
case 'N': return W();
case 'W': return S();
case 'S': return E();
case 'E': return N();
}
return N();
}
Orientation turnRight() const {
switch (dir_) {
case 'N': return E();
case 'E': return S();
case 'S': return W();
case 'W': return N();
}
return N();
}
char getDir() const { return dir_; }
bool operator==(const Orientation& other) const {
return dir_ == other.dir_;
}
private:
explicit Orientation(char d) : dir_(d) {}
char dir_;
};
// 指令函数:作为函数指针传入测试数据
inline Orientation TurnLeft(const Orientation& o) { return o.turnLeft(); }
inline Orientation TurnRight(const Orientation& o) { return o.turnRight(); }
2. orientation.param — 纯数据文件
// { 初始方向, 指令函数, 期望方向 }
{Orientation::N(), TurnRight, Orientation::E()},
{Orientation::N(), TurnLeft, Orientation::W()},
{Orientation::E(), TurnLeft, Orientation::N()},
{Orientation::E(), TurnRight, Orientation::S()},
{Orientation::S(), TurnLeft, Orientation::E()},
{Orientation::S(), TurnRight, Orientation::W()},
{Orientation::W(), TurnLeft, Orientation::S()},
{Orientation::W(), TurnRight, Orientation::N()},
增加方向或指令只需在这里加一行,测试代码完全不动。
3. orientation_test.cpp
#include <gtest/gtest.h>
#include <tuple>
#include "orientation.h"
// 函数指针类型别名,让 tuple 类型更易读
using TurnFn = Orientation(*)(const Orientation&);
using ParamType = std::tuple<Orientation, TurnFn, Orientation>;
// ───────────────────────────────────────────
// 数据从外部文件引入(在宏外部 #include 完全合法)
// ───────────────────────────────────────────
static const ParamType kParams[] = {
#include "orientation.param"
};
// ───────────────────────────────────────────
// 参数化测试类
// ───────────────────────────────────────────
class OrientationTurnTest : public ::testing::TestWithParam<ParamType> {};
// ───────────────────────────────────────────
// 测试逻辑:只写一次
// ───────────────────────────────────────────
TEST_P(OrientationTurnTest, TurnOperations) {
auto [origin, func, expected] = GetParam(); // C++17 结构化绑定
Orientation result = func(origin);
ASSERT_EQ(result.getDir(), expected.getDir())
<< "From " << origin.getDir()
<< " expected " << expected.getDir()
<< " but got " << result.getDir();
}
// ───────────────────────────────────────────
// 实例化:数据来自 kParams 数组
// ───────────────────────────────────────────
INSTANTIATE_TEST_SUITE_P(
left_right_data,
OrientationTurnTest,
::testing::ValuesIn(kParams)
);
4. CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(MarsRoverTest CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(GTest REQUIRED)
add_executable(orientation_test
test/orientation_test.cpp
)
target_include_directories(orientation_test PRIVATE
src
test # 让编译器找到 orientation.param
)
target_link_libraries(orientation_test PRIVATE
GTest::gtest_main
)
enable_testing()
include(GoogleTest)
gtest_discover_tests(orientation_test)
https://godbolt.org/z/5n17hnG3v
运行结果
[==========] Running 8 tests from 1 test suite.
[ RUN ] left_right_data/OrientationTurnTest.TurnOperations/0 N→TurnRight→E OK
[ RUN ] left_right_data/OrientationTurnTest.TurnOperations/1 N→TurnLeft →W OK
[ RUN ] left_right_data/OrientationTurnTest.TurnOperations/2 E→TurnLeft →N OK
[ RUN ] left_right_data/OrientationTurnTest.TurnOperations/3 E→TurnRight→S OK
[ RUN ] left_right_data/OrientationTurnTest.TurnOperations/4 S→TurnLeft →E OK
[ RUN ] left_right_data/OrientationTurnTest.TurnOperations/5 S→TurnRight→W OK
[ RUN ] left_right_data/OrientationTurnTest.TurnOperations/6 W→TurnLeft →S OK
[ RUN ] left_right_data/OrientationTurnTest.TurnOperations/7 W→TurnRight→N OK
[==========] 8 tests passed.
数据与逻辑分离的结构
orientation.param ← 只管数据,产品经理/测试人员可直接编辑
orientation_test.cpp ← 只管逻辑,开发人员维护
orientation.h ← 只管实现,完全不感知测试存在
三个文件职责清晰,互不干扰。
测试对象的选择
1⃣ 测试对象选择原则
在 遵循设计原则(如单一职责 SRP、依赖倒置 DIP)的系统中:
- 模块(Module) 是最好的测试对象。
- 原因:
- 模块内聚高,边界清晰 → 易于隔离测试
- 与外部依赖解耦 → 测试稳定、可控
- 可单元化执行 → 便于自动化
- 原因:
- 测试对象应选择接口清晰、依赖可控的模块,而非随意的类或函数。
2⃣ UML 类图解析
理解
- class1、class2、class3
- 系统中的普通类
- class1依赖 class2 和 class3 → 表示模块之间调用关系
- class3依赖 class2 → 进一步表示模块层次依赖
- interface
- <> 标签表示它是一个接口
- class3依赖 interface → 依赖倒置原则(DIP)示例
- 通过接口解耦模块
- Implement
- 实现 interface 的具体类(realizes)
- 测试时可以用 Mock 或 Stub 替代 Implement 来隔离 class3
- 箭头意义
-->普通依赖 / 使用关系..|>接口实现关系
3⃣ 测试对象选择示例
假设要测试 class3:
// class3依赖 interface
class3 obj;
// 使用Mock替代真实实现
class MockImplement : public interface {
void someMethod() override { /* 返回可控结果 */ }
};
MockImplement mock;
obj.setInterface(&mock); // 注入依赖
- 好处:
- 测试 class3 时无需真实 Implement
- 保证测试独立性
- 更容易覆盖边界条件和异常情况
4⃣ 总结
- 测试对象应尽量选择模块而非单个类函数。
- 接口和依赖注入(DI)是提高测试可控性的重要手段。
- UML 类图可以清晰描述模块依赖和接口实现,为测试设计提供参考。
通信设备的例子
1⃣ 分层通信模型
理解
- 客户端和服务器端各自分层
- 应用层(C_App / S_App)
- 处理具体业务逻辑,如聊天应用、HTTP请求/响应
- 对用户透明
- TLS 层(C_TLS / S_TLS)
- 负责加密通信(传输层安全 TLS)
- 提供加密、身份验证、消息完整性
- 链路层(C_Link / S_Link)
- 负责网络数据帧传输
- 可以通过以太网或其他链路协议
- 物理层(Physical)
- 真正的电信号或光信号传输
- 应用层(C_App / S_App)
- 层间连接
- 每一层只与上下相邻层通信 → 遵循 分层设计
- 便于测试和替换实现(例如 TLS 层可以替换为 MockTLS 进行单元测试)
// C++伪代码:分层模块设计
class TLSLayer {
public:
void encrypt(const std::string& data);
std::string decrypt(const std::string& data);
};
class AppLayer {
TLSLayer& tls;
public:
void sendMessage(const std::string& msg) {
tls.encrypt(msg);
}
};
2⃣ TLS 握手序列
理解
- ClientHello
- 客户端发起 TLS 握手
- 包含客户端支持的加密套件、随机数等
- ServerHello + Certificate + ServerKeyExchange + CertificateRequest + ServerHelloDone
- 服务器回应客户端
- 发送自己的证书
- 提供密钥交换信息
- 请求客户端证书(可选)
- 表明 TLS 握手服务器端部分完成
理解
- 客户端回应
- Certificate:发送客户端证书(如服务器要求)
- ClientKeyExchange:密钥交换信息
- CertificateVerify:验证客户端身份
- ChangeCipherSpec:切换到加密通信
- Finished:握手完成标志
- 服务器回应 ChangeCipherSpec + Finished
- 双方加密协商完成
- TLS 握手结束
- 应用数据传输
- 客户端和服务器开始加密通信
- 用户应用数据通过 TLS 层传输
3⃣ 软件工程设计要点
- 关注点分离(Separation of Concerns)
- 应用层、TLS层、链路层解耦
- 每一层单独测试
- 可测性增强
- TLS 层可以使用 MockTLS 进行单元测试
- 链路层可以模拟网络延迟或丢包
- 数据驱动与模拟
- 可以用测试向量模拟握手消息和加密数据
- 避免依赖真实网络
// 伪代码:使用 MockTLS 测试 AppLayer
class MockTLS : public TLSLayer {
public:
void encrypt(const std::string& data) override {
encrypted = "mock:" + data;
}
};
MockTLS mock;
AppLayer app{mock};
app.sendMessage("Hello");
// 验证 mock.encrypted 是否正确
4⃣ 总结
- 分层图(Mermaid Graph) → 描述系统结构
- 序列图(Mermaid SequenceDiagram) → 描述 TLS 握手流程
- 设计思想
- 分层解耦
- 接口可替换
- 数据驱动测试
- C++实践
- 每层模块化,依赖注入
- 可用 Mock 替换真实实现
- 提高可测性和可靠性
测试对象选择示意图
1⃣ Mermaid 图结构解析
理解
- 测试对象层次
- Upper_Layer(交互指令层)
- 模拟客户端和服务器交互的操作,例如:
StartConnect:启动连接Send:发送消息Receive:接收消息
- 模拟客户端和服务器交互的操作,例如:
- TLS 客户端 / 服务端
- 被测试对象(System Under Test, SUT)
- 实现 TLS 协议逻辑、加密/解密、握手等
- FakeChannel(伪通道)
- 模拟底层通信信道
- 避免依赖真实网络,增加可测性
- 可以控制延迟、丢包、顺序等
- Upper_Layer(交互指令层)
- 连接关系
SC1 --> Client、S1 --> Client:交互指令传给 TLS 客户端Client --> R1:客户端处理完后的输出反馈Client <==> FakeChannel:客户端通过伪通道进行通信Server <==> FakeChannel:服务器通过伪通道接收客户端消息
- 测试设计要点
- 分离关注点(Separation of Concerns):
- 交互指令、TLS逻辑、通信通道各自独立
- 便于单元测试和集成测试
- 可测性增强:
- FakeChannel 可以替换成 MockChannel 或记录消息日志
- 可以验证消息顺序、内容正确性
- 分离关注点(Separation of Concerns):
2⃣ C++ 测试示例
#include <gtest/gtest.h>
#include <string>
#include <vector>
// 模拟消息通道
class FakeChannel {
public:
void send(const std::string& msg) {
messages.push_back(msg);
}
std::string receive() {
if (!messages.empty()) {
std::string msg = messages.front();
messages.erase(messages.begin());
return msg;
}
return "";
}
private:
std::vector<std::string> messages;
};
// TLS 客户端
class TLSClient {
public:
TLSClient(FakeChannel& ch) : channel(ch) {}
void startConnect() {
// 模拟连接逻辑
channel.send("ClientHello");
}
void send(const std::string& msg) {
channel.send(msg);
}
std::string receive() {
return channel.receive();
}
private:
FakeChannel& channel;
};
// TLS 服务端
class TLSServer {
public:
TLSServer(FakeChannel& ch) : channel(ch) {}
void process() {
std::string msg = channel.receive();
if (msg == "ClientHello") {
channel.send("ServerHello");
}
}
private:
FakeChannel& channel;
};
// 测试用例
TEST(TLSTest, ClientServerInteraction) {
FakeChannel channel;
TLSClient client(channel);
TLSServer server(channel);
client.startConnect();
server.process();
std::string response = client.receive();
ASSERT_EQ(response, "ServerHello");
}
说明
FakeChannel代替真实网络,保证测试可重复TLSClient/TLSServer是测试对象- 测试关注点:
- 客户端发起握手
- 服务器正确响应
- 消息顺序和内容正确
3⃣ 总结
- 测试对象选择原则:
- 优先选择模块或类级别(SUT)
- 依赖外部系统的部分通过 Mock / Fake 替换
- 保持交互清晰、可观察
- Mermaid 图体现的设计思想:
- 上层指令 → 被测模块 → 底层通道
- 分层、解耦、可测试
- C++ 实现要点:
- 依赖注入(DI)使模块更容易替换依赖
- Fake / Mock 提高单元测试独立性
- 断言消息内容和顺序验证系统行为
更多推荐


所有评论(0)