完整的火星车方向转向例子,数据用 .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) 是最好的测试对象。
    • 原因:
      1. 模块内聚高,边界清晰 → 易于隔离测试
      2. 与外部依赖解耦 → 测试稳定、可控
      3. 可单元化执行 → 便于自动化
  • 测试对象应选择接口清晰、依赖可控的模块,而非随意的类或函数。

2⃣ UML 类图解析

realizes

class1

class2

class3

«interface»

interface

Implement

理解

  1. class1、class2、class3
    • 系统中的普通类
    • class1依赖 class2 和 class3 → 表示模块之间调用关系
    • class3依赖 class2 → 进一步表示模块层次依赖
  2. interface
    • <> 标签表示它是一个接口
    • class3依赖 interface → 依赖倒置原则(DIP)示例
    • 通过接口解耦模块
  3. Implement
    • 实现 interface 的具体类(realizes)
    • 测试时可以用 Mock 或 Stub 替代 Implement 来隔离 class3
  4. 箭头意义
    • --> 普通依赖 / 使用关系
    • ..|> 接口实现关系

3⃣ 测试对象选择示例

假设要测试 class3:

// class3依赖 interface
class3 obj;
// 使用Mock替代真实实现
class MockImplement : public interface {
    void someMethod() override { /* 返回可控结果 */ }
};
MockImplement mock;
obj.setInterface(&mock); // 注入依赖
  • 好处
    1. 测试 class3 时无需真实 Implement
    2. 保证测试独立性
    3. 更容易覆盖边界条件和异常情况

4⃣ 总结

  1. 测试对象应尽量选择模块而非单个类函数。
  2. 接口和依赖注入(DI)是提高测试可控性的重要手段。
  3. UML 类图可以清晰描述模块依赖和接口实现,为测试设计提供参考。

通信设备的例子

1⃣ 分层通信模型

服务器端

客户端

应用层 客户端

TLS 客户端

链路层

应用层 服务器端

TLS 服务器端

链路层

物理层

理解

  • 客户端和服务器端各自分层
    1. 应用层(C_App / S_App)
      • 处理具体业务逻辑,如聊天应用、HTTP请求/响应
      • 对用户透明
    2. TLS 层(C_TLS / S_TLS)
      • 负责加密通信(传输层安全 TLS)
      • 提供加密、身份验证、消息完整性
    3. 链路层(C_Link / S_Link)
      • 负责网络数据帧传输
      • 可以通过以太网或其他链路协议
    4. 物理层(Physical)
      • 真正的电信号或光信号传输
  • 层间连接
    • 每一层只与上下相邻层通信 → 遵循 分层设计
    • 便于测试和替换实现(例如 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 握手序列

TLS Server TLS Client TLS Server TLS Client 包含证书、密钥交换及客户端证书请求 ClientHello ServerHello Certificate ServerKeyExchange CertificateRequest ServerHelloDone

理解

  • ClientHello
    • 客户端发起 TLS 握手
    • 包含客户端支持的加密套件、随机数等
  • ServerHello + Certificate + ServerKeyExchange + CertificateRequest + ServerHelloDone
    • 服务器回应客户端
    • 发送自己的证书
    • 提供密钥交换信息
    • 请求客户端证书(可选)
    • 表明 TLS 握手服务器端部分完成
S C S C 客户端回应并验证 应用数据 (Application Data) Certificate ClientKeyExchange CertificateVerify ChangeCipherSpec Finished ChangeCipherSpec Finished 应用数据 应用数据

理解

  • 客户端回应
    • Certificate:发送客户端证书(如服务器要求)
    • ClientKeyExchange:密钥交换信息
    • CertificateVerify:验证客户端身份
    • ChangeCipherSpec:切换到加密通信
    • Finished:握手完成标志
  • 服务器回应 ChangeCipherSpec + Finished
    • 双方加密协商完成
    • TLS 握手结束
  • 应用数据传输
    • 客户端和服务器开始加密通信
    • 用户应用数据通过 TLS 层传输

3⃣ 软件工程设计要点

  1. 关注点分离(Separation of Concerns)
    • 应用层、TLS层、链路层解耦
    • 每一层单独测试
  2. 可测性增强
    • TLS 层可以使用 MockTLS 进行单元测试
    • 链路层可以模拟网络延迟或丢包
  3. 数据驱动与模拟
    • 可以用测试向量模拟握手消息和加密数据
    • 避免依赖真实网络
// 伪代码:使用 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 握手流程
  • 设计思想
    1. 分层解耦
    2. 接口可替换
    3. 数据驱动测试
  • C++实践
    • 每层模块化,依赖注入
    • 可用 Mock 替换真实实现
    • 提高可测性和可靠性

测试对象选择示意图

1⃣ Mermaid 图结构解析

交互指令

StartConnect

Send

Receive

StartConnect

Send

Receive

TLS 客户端 (TLS Client)

TLS 服务端 (TLS Server)

Fake通道 (Fake Channel)

理解

  1. 测试对象层次
    • Upper_Layer(交互指令层)
      • 模拟客户端和服务器交互的操作,例如:
        • StartConnect:启动连接
        • Send:发送消息
        • Receive:接收消息
    • TLS 客户端 / 服务端
      • 被测试对象(System Under Test, SUT)
      • 实现 TLS 协议逻辑、加密/解密、握手等
    • FakeChannel(伪通道)
      • 模拟底层通信信道
      • 避免依赖真实网络,增加可测性
      • 可以控制延迟、丢包、顺序等
  2. 连接关系
    • SC1 --> ClientS1 --> Client:交互指令传给 TLS 客户端
    • Client --> R1:客户端处理完后的输出反馈
    • Client <==> FakeChannel:客户端通过伪通道进行通信
    • Server <==> FakeChannel:服务器通过伪通道接收客户端消息
  3. 测试设计要点
    • 分离关注点(Separation of Concerns)
      • 交互指令、TLS逻辑、通信通道各自独立
      • 便于单元测试和集成测试
    • 可测性增强
      • FakeChannel 可以替换成 MockChannel 或记录消息日志
      • 可以验证消息顺序、内容正确性

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");
}

说明

  1. FakeChannel 代替真实网络,保证测试可重复
  2. TLSClient / TLSServer 是测试对象
  3. 测试关注点:
    • 客户端发起握手
    • 服务器正确响应
    • 消息顺序和内容正确

3⃣ 总结

  • 测试对象选择原则
    1. 优先选择模块或类级别(SUT)
    2. 依赖外部系统的部分通过 Mock / Fake 替换
    3. 保持交互清晰、可观察
  • Mermaid 图体现的设计思想
    • 上层指令 → 被测模块 → 底层通道
    • 分层、解耦、可测试
  • C++ 实现要点
    • 依赖注入(DI)使模块更容易替换依赖
    • Fake / Mock 提高单元测试独立性
    • 断言消息内容和顺序验证系统行为
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐