输入关键词开始搜索

测试计划文档

08 — 云融(YunRong)· 测试计划文档

前置文档05-模块详细设计文档06-接口设计文档 设计思路00-设计思路文档

版本:v0.1 状态:已发布 最后更新:2025-01


1. 测试策略概览

1.1 测试金字塔

           ┌─────────┐
           │  E2E    │  5%  — 完整用户场景 (Mock Server + 真实 GUI)
           │  端到端  │
          ┌┴─────────┴┐
          │ Integration│  20% — 模块间交互 (网络↔数据↔业务)
          │   集成测试  │
         ┌┴───────────┴┐
         │   Unit Test  │  75% — 独立类/函数 (纯逻辑、无 IO)
         │   单元测试    │
        └───────────────┘
层级覆盖目标运行时间执行频率
单元测试所有纯逻辑类 ≥ 70% 行覆盖< 10s每次 commit
集成测试核心数据流路径 100% 覆盖< 60s每次 push
端到端测试核心用户场景 100% 覆盖< 5min每次 PR / 每日

1.2 测试框架选型

框架用途选择理由
Google Test (gtest)C++ 单元/集成测试业界标准,断言丰富,与 CMake 深度集成
Google Mock (gmock)Mock 对象生成与 gtest 配套,自动生成 Mock 类
Qt TestGUI 组件测试Qt 原生,可模拟鼠标/键盘事件
Mock Server (自研)网络集成测试已在 06-API设计文档 §6 中设计

1.3 编译与运行

# tests/CMakeLists.txt
enable_testing()

# Google Test
find_package(GTest REQUIRED)

# 单元测试可执行文件
add_executable(enterprise_tests
    unit/test_protocol.cpp
    unit/test_reconnect_strategy.cpp
    unit/test_message_model.cpp
    unit/test_sync_manager.cpp
    unit/test_config_manager.cpp
    # ...
    integration/test_ws_reconnect.cpp
    integration/test_file_transfer.cpp
    integration/test_offline_sync.cpp
    # ...
)

target_link_libraries(enterprise_tests PRIVATE
    enterprise_core
    GTest::gtest
    GTest::gmock
    Qt6::Test
)

# 注册到 CTest
add_test(NAME enterprise_tests COMMAND enterprise_tests)

运行:

# 全量
ctest --test-dir build -j4 --output-on-failure

# 仅单元测试
./build/tests/enterprise_tests --gtest_filter='Unit*'

# 带覆盖率
cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON ..
cmake --build . -j4
ctest --test-dir build
lcov --capture --directory build --output-file coverage.info
genhtml coverage.info --output-directory coverage_report

2. 单元测试设计

2.1 测试目标清单

按优先级排序

优先级目标类理由预估用例数
🔴 P0Protocol纯函数、协议正确性是系统基石15+
🔴 P0ReconnectStrategy纯算法,边界条件多8+
🔴 P0RateLimiter令牌桶算法,并发安全6+
🟡 P1SyncManager同步逻辑复杂,易出错10+ (需 Mock IDataStore)
🟡 P1IMService核心业务逻辑8+ (需 Mock IDataStore + ConnMgr)
🟡 P1MessageListModelQt Model 角色映射5+
🟢 P2ConfigManager线程安全单例4+
🟢 P2MessageBus无锁队列3+
🟢 P2SqliteStore需要真实 SQLite 文件按需

2.2 典型单元测试示例

Protocol 测试(纯函数、无依赖、最高优先级)

// tests/unit/test_protocol.cpp
#include <gtest/gtest.h>
#include "net/protocol.h"

using namespace core::net;

class ProtocolTest : public ::testing::Test {
protected:
    Protocol proto_;
};

// ---- JSON 编码 ----

TEST_F(ProtocolTest, EncodeAuth_CreatesValidJson) {
    auto json = proto_.encodeAuth("test_token_abc", 1);
    auto frame = nlohmann::json::parse(json);

    EXPECT_EQ(frame["ver"], 1);
    EXPECT_EQ(frame["type"], "auth");
    EXPECT_EQ(frame["seq"], 1);
    EXPECT_EQ(frame["payload"]["token"], "test_token_abc");
    EXPECT_TRUE(frame["ts"].is_number());
}

TEST_F(ProtocolTest, EncodeMessage_HandlesAllContentTypes) {
    model::Message msg;
    msg.convId = 2001;
    msg.senderId = 1001;
    msg.contentType = 0; // text
    msg.contentBody = R"({"type":"text","text":"hello"})";

    auto json = proto_.encodeMessage(msg, 42);
    auto frame = nlohmann::json::parse(json);

    EXPECT_EQ(frame["type"], "msg");
    EXPECT_EQ(frame["seq"], 42);
    EXPECT_EQ(frame["payload"]["conv_id"], 2001);
}

// ---- JSON 解码 ----

TEST_F(ProtocolTest, Decode_ValidMessageFrame_ReturnsCorrectType) {
    std::string json = R"({
        "ver": 1, "type": "msg", "seq": 100, "ts": 1704067200000,
        "payload": { "msg_id": "abc", "conv_id": 2001, "sender_id": 1002,
                      "content_type": 0, "content_body": {"type":"text","text":"hi"} }
    })";

    auto result = proto_.decode(json);
    EXPECT_EQ(result.type, Protocol::DecodedType::Message);
    EXPECT_EQ(result.seq, 100);
}

TEST_F(ProtocolTest, Decode_AckFrame_ReturnsCorrectType) {
    std::string json = R"({
        "ver": 1, "type": "ack", "seq": 0, "ts": 1704067200000,
        "payload": { "ack_seq": 42, "status": "ok" }
    })";

    auto result = proto_.decode(json);
    EXPECT_EQ(result.type, Protocol::DecodedType::Ack);
    EXPECT_EQ(result.payload["ack_seq"], 42);
}

// ---- 校验 ----

TEST_F(ProtocolTest, Validate_MissingTypeField_ReturnsError) {
    std::string json = R"({"ver": 1, "seq": 1, "ts": 1, "payload": {}})";
    auto error = proto_.validate(nlohmann::json::parse(json));
    EXPECT_TRUE(error.has_value());
}

TEST_F(ProtocolTest, Validate_UnknownFrameType_ReturnsError) {
    std::string json = R"({"ver": 1, "type": "unknown_xyz", "seq": 1, "ts": 1, "payload": {}})";
    auto error = proto_.validate(nlohmann::json::parse(json));
    EXPECT_TRUE(error.has_value());
}

// ---- 二进制帧 ----

TEST_F(ProtocolTest, EncodeFileChunk_ProducesCorrectLayout) {
    uint8_t data[] = {0xAA, 0xBB, 0xCC};
    auto frame = proto_.encodeFileChunk(5, 5242880, data, 3);

    // Total Length (4B) + Type (1B) + Chunk Index (2B) + Offset (4B) + Data (3B)
    EXPECT_EQ(frame.size(), 4 + 1 + 2 + 4 + 3);
    EXPECT_EQ(frame[4], 0x02);  // Type = FILE_CHUNK
    EXPECT_EQ(frame[5], 0x00);  // Chunk Index high byte (5 = 0x0005)
    EXPECT_EQ(frame[6], 0x05);  // Chunk Index low byte
}

// ---- 序列号提取 ----

TEST_F(ProtocolTest, ExtractSeq_MultipleFrames_HandlesCorrectly) {
    auto frame1 = nlohmann::json::parse(R"({"seq": 1})");
    auto frame2 = nlohmann::json::parse(R"({"seq": 0})");
    auto frame3 = nlohmann::json::parse(R"({"seq": 2147483648})");

    EXPECT_EQ(Protocol::extractSeq(frame1), 1);
    EXPECT_EQ(Protocol::extractSeq(frame2), 0);
    EXPECT_EQ(Protocol::extractSeq(frame3), 2147483648);
}

ReconnectStrategy 测试(纯算法、边界值覆盖)

// tests/unit/test_reconnect_strategy.cpp
#include <gtest/gtest.h>
#include "net/reconnect_strategy.h"

using namespace core::net;

TEST(ReconnectStrategyTest, DelaysFollowExponentialBackoff) {
    ReconnectStrategy strategy;

    // attempt 1: 1s
    EXPECT_EQ(strategy.nextDelay(), std::chrono::milliseconds(1000));
    // attempt 2: 2s
    EXPECT_EQ(strategy.nextDelay(), std::chrono::milliseconds(2000));
    // attempt 3: 4s
    EXPECT_EQ(strategy.nextDelay(), std::chrono::milliseconds(4000));
    // attempt 4: 8s
    EXPECT_EQ(strategy.nextDelay(), std::chrono::milliseconds(8000));
    // attempt 5: 30s (transition)
    EXPECT_EQ(strategy.nextDelay(), std::chrono::milliseconds(30000));
    // attempt 6: 60s (cap)
    EXPECT_EQ(strategy.nextDelay(), std::chrono::milliseconds(60000));
}

TEST(ReconnectStrategyTest, DelayCapsAt60Seconds) {
    ReconnectStrategy strategy;
    for (int i = 0; i < 5; i++) strategy.nextDelay(); // skip to attempt 6

    for (int i = 0; i < 10; i++) {
        EXPECT_EQ(strategy.nextDelay(), std::chrono::milliseconds(60000));
    }
}

TEST(ReconnectStrategyTest, ShouldRetry_ReturnsFalseAfterMaxRetries) {
    ReconnectStrategy strategy;
    for (int i = 0; i < ReconnectStrategy::MAX_RETRIES; i++) {
        ASSERT_TRUE(strategy.shouldRetry());
        strategy.nextDelay();
    }
    EXPECT_FALSE(strategy.shouldRetry());
}

TEST(ReconnectStrategyTest, Reset_RestoresInitialState) {
    ReconnectStrategy strategy;
    strategy.nextDelay();
    strategy.nextDelay();
    EXPECT_EQ(strategy.attempt(), 2);

    strategy.reset();
    EXPECT_EQ(strategy.attempt(), 0);
    EXPECT_EQ(strategy.nextDelay(), std::chrono::milliseconds(1000));
}

ConfigManager 测试(线程安全单例)

// tests/unit/test_config_manager.cpp
#include <gtest/gtest.h>
#include <thread>
#include <vector>
#include "infra/config_mgr.h"

using namespace core::infra;

TEST(ConfigManagerTest, SingletonReturnsSameInstance) {
    auto& a = ConfigManager::instance();
    auto& b = ConfigManager::instance();
    EXPECT_EQ(&a, &b);
}

TEST(ConfigManagerTest, SetAndGet_String) {
    auto& cfg = ConfigManager::instance();
    cfg.setRaw("test.key", "hello");
    auto val = cfg.getRaw("test.key");
    ASSERT_TRUE(val.has_value());
    EXPECT_EQ(*val, "hello");
}

TEST(ConfigManagerTest, GetMissingKey_ReturnsNullopt) {
    auto& cfg = ConfigManager::instance();
    auto val = cfg.getRaw("nonexistent.key.xyz");
    EXPECT_FALSE(val.has_value());
}

TEST(ConfigManagerTest, TemplateGet_WithDefault) {
    auto& cfg = ConfigManager::instance();
    cfg.setRaw("int.key", "42");
    EXPECT_EQ(cfg.get<int>("int.key", 0), 42);
    EXPECT_EQ(cfg.get<int>("missing.key", 99), 99);
}

TEST(ConfigManagerTest, ThreadSafety_ConcurrentReadWrite) {
    auto& cfg = ConfigManager::instance();
    cfg.setRaw("ts.key", "start");

    std::vector<std::thread> threads;
    for (int i = 0; i < 10; i++) {
        threads.emplace_back([i, &cfg] {
            for (int j = 0; j < 100; j++) {
                cfg.setRaw("ts.key", std::to_string(i * 100 + j));
                auto val = cfg.getRaw("ts.key");
                ASSERT_TRUE(val.has_value()); // 永远不应该得到 nullopt
            }
        });
    }
    for (auto& t : threads) t.join();
    // 无崩溃、无数据竞争 = 通过
}

3. 集成测试设计

3.1 Mock 策略

被测对象 (SUT)           外部依赖              Mock 方式
──────────────────────────────────────────────────────────
IMService          → IDataStore           MockDataStore (gmock)
                   → ConnectionManager    MockConnectionManager (手写)
SyncManager        → IDataStore (local)   SqliteStore (真实,:memory:)
                   → WebSocket sender     lambda 捕获
ConnectionManager  → IConnection (WS)     MockConnection (gmock)
                   → Protocol             真实 Protocol (无副作用)
FileService        → ConnectionManager    MockConnectionManager
                   → IDataStore           MockDataStore

3.2 核心集成测试场景

场景 1:消息发送 → ACK → 状态更新

GIVEN: MockConnectionManager (捕获 sendText 调用)
       MockDataStore (捕获 storeMessage 调用)
  WHEN: IMService::sendTextMessage(convId=2001, text="hello")
  THEN: 1. storeMessage 被调用 1 次 (synced=0)
        2. sendText 被调用 1 次 (含正确 JSON)
  WHEN: 模拟收到 ACK (seq 对应)
  THEN: 3. markMessageSynced 被调用 1 次
        4. updateMessageStatus → Sent

场景 2:WebSocket 断线 → 重连 → 增量同步

GIVEN: 真实 Mock Server (WS port 18001)
       SqliteStore (内存模式,含 50 条预置消息)
  WHEN: 建立 WS 连接,auth 成功
        收到 30 条消息后 Mock Server 主动断开连接
  THEN: 1. ConnectionManager 状态 → Reconnecting
        2. ReconnectStrategy 按指数退避重试
        3. 重连成功后发送 sync(last_seq=30)
        4. 收到 sync_data (seq 31-50)
        5. SqliteStore 中消息完整 (50 条,无重复)
        6. 状态 → Connected

场景 3:离线队列重放

GIVEN: SqliteStore 中有 3 条未同步消息 (synced=0)
       offline_queue 中有 3 条对应操作
       MockConnectionManager (捕获 sendText)
  WHEN: SyncManager::replayOfflineQueue()
  THEN: 1. sendText 被调用 3 次
        2. 每次成功后 → markOfflineOpDone
        3. 全部完成后 → 队列为空
  WHEN: 其中第 2 条 sendText 失败 (Mock 返回 false)
  THEN: 4. 该条 retry_count++
        5. retry_count < 3 → 保留在队列
        6. retry_count >= 3 → markOfflineOpFailed

场景 4:文件分块上传 + 断点续传

GIVEN: Mock Server (文件端口 18003)
       本地文件 test.bin (5MB, SHA-256 已知)
  WHEN: FileService::uploadFile("test.bin", chunkSize=1MB)
  THEN: 1. POST /files/check → { exists: false }
        2. 分配 upload_id
        3. POST chunk 0-4 依次发送
        4. 每发送 1 块 → progress 回调触发
        5. POST /files/upload/{id}/complete → { url, hash_verified: true }
  WHEN: 上传 chunk 2 时 Mock Server 返回 500
  THEN: 6. chunk 2 标记失败、自动重试
        7. 重试成功后继续 chunk 3, 4
  WHEN: 上传中途断开网络
        恢复后调用 resumeTransfer
  THEN: 8. 从 completed_chunks bitmap 恢复
        9. 只发送未完成的块
       10. 最终 5 块全部完成

4. 端到端测试设计

4.1 E2E 场景清单

编号场景Mock Server 场景文件预期结果
E2E-01登录 → 加载会话列表 → 发送消息 → 收到回复normal_chat.json消息气泡正确显示,状态从”发送中”→“已送达”
E2E-02接收 200 条离线消息 → 分批同步offline_sync.json消息分批出现,最后一条后显示”同步完成”
E2E-03发送消息时断网 → 恢复 → 消息自动重发network_loss.json气泡先显示”发送失败”,恢复后自动变为”已送达”
E2E-04拖拽文件到聊天窗口 → 上传 → 自动发送文件消息file_upload.json进度条完整,上传完成后文件卡片出现在聊天中
E2E-05收到新任务通知 → 点击跳转 → 处理任务task_notify.jsonToast 弹出 → 点击 → 切换到任务视图 → 可处理
E2E-06切换暗色主题 → 所有窗口即时更新N/A (纯本地)列表、气泡、控件全部切换为暗色

4.2 E2E 测试基础设施

// tests/e2e/e2e_test_fixture.h
class E2ETestFixture : public ::testing::Test {
protected:
    void SetUp() override {
        // 1. 启动 Mock Server 子进程
        mockServer_ = std::make_unique<QProcess>();
        mockServer_->start("./mock_server",
            {"--scenario", scenarioPath_, "--ws-port", "18001",
             "--http-port", "18002", "--file-port", "18003"});
        ASSERT_TRUE(mockServer_->waitForStarted(5000));

        // 2. 初始化应用核心
        threadPool_ = std::make_shared<ThreadPool>(2);
        store_ = std::make_shared<SqliteStore>(":memory:");
        net_ = std::make_shared<ConnectionManager>(*threadPool_);
        imService_ = std::make_shared<IMService>(net_, store_);

        // 3. 显示测试窗口
        mainWin_ = std::make_unique<MainWindow>();
        mainWin_->setServices(imService_, fileService_, taskService_);
        mainWin_->show();
    }

    void TearDown() override {
        mainWin_->close();
        mockServer_->terminate();
        mockServer_->waitForFinished(3000);
    }

    // 辅助方法
    void loginAs(const std::string& username) {
        // 通过 REST API 登录
        // 建立 WS 连接
        // 等待 auth_ok
    }

    void waitForSignal(QObject* obj, const char* signal, int timeoutMs = 2000) {
        QSignalSpy spy(obj, signal);
        ASSERT_TRUE(spy.wait(timeoutMs));
    }

    std::string scenarioPath_;
    std::unique_ptr<QProcess> mockServer_;
    std::shared_ptr<ThreadPool> threadPool_;
    std::shared_ptr<IDataStore> store_;
    std::shared_ptr<ConnectionManager> net_;
    std::shared_ptr<IMService> imService_;
    std::unique_ptr<MainWindow> mainWin_;
};

5. 性能测试

5.1 性能基准

指标基准值测试方法严重阈值
消息发送延迟< 200ms时间戳打点,1000 条取 P99> 500ms
消息吞吐量≥ 100 条/s压力测试 10s< 50 条/s
10000 条消息列表滚动60fpsQElapsedTimer + frame counter< 30fps
SQLite 批量插入 (200条)< 50ms事务包裹> 200ms
消息搜索 (FTS5, 10万条)< 5ms计时 10 次取平均> 50ms
内存占用 (空闲)< 150 MB持续运行 1h 采样> 300 MB
冷启动时间< 3sQElapsedTimer> 5s

5.2 性能测试代码骨架

// tests/performance/perf_messages.cpp
TEST(PerformanceTest, MessageSendLatency_P99Under200ms) {
    MockServer server("normal_chat.json");
    ConnectionManager net(threadPool);

    std::vector<double> latencies;
    latencies.reserve(1000);

    for (int i = 0; i < 1000; i++) {
        auto start = std::chrono::high_resolution_clock::now();
        net.sendMessage(makeTestMsg(i), [&](bool ok) {
            auto end = std::chrono::high_resolution_clock::now();
            latencies.push_back(
                std::chrono::duration<double, std::milli>(end - start).count()
            );
        });
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }

    std::sort(latencies.begin(), latencies.end());
    double p99 = latencies[static_cast<size_t>(latencies.size() * 0.99)];

    EXPECT_LT(p99, 200.0) << "P99 latency: " << p99 << "ms";
}

6. CI/CD 集成

6.1 GitHub Actions 流水线

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [develop, main]
  pull_request:
    branches: [main]

jobs:
  build-and-test:
    strategy:
      matrix:
        os: [ubuntu-22.04, windows-2022]
        build_type: [Debug, Release]

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4

      - name: Install Dependencies (Linux)
        if: runner.os == 'Linux'
        run: |
          sudo apt-get update
          sudo apt-get install -y qt6-base-dev qt6-charts-dev \
            qt6-websockets-dev libsqlite3-dev \
            libgtest-dev nlohmann-json3-dev libspdlog-dev

      - name: Install Dependencies (Windows)
        if: runner.os == 'Windows'
        run: |
          choco install qt6 gtest nlohmann-json spdlog

      - name: Configure CMake
        run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DBUILD_TESTS=ON

      - name: Build
        run: cmake --build build -j$(nproc)

      - name: Unit Tests
        run: ctest --test-dir build -L unit --output-on-failure -j4

      - name: Integration Tests
        run: ctest --test-dir build -L integration --output-on-failure

      - name: Coverage (Debug only)
        if: matrix.build_type == 'Debug' && runner.os == 'Linux'
        run: |
          lcov --capture --directory build --output-file coverage.info
          lcov --remove coverage.info '/usr/*' '*/tests/*' '*/thirdparty/*' \
            --output-file coverage_filtered.info
          genhtml coverage_filtered.info --output-directory coverage_report

      - name: Upload Coverage
        if: matrix.build_type == 'Debug' && runner.os == 'Linux'
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage_report/

6.2 测试标签

# CMakeLists.txt 中按标签分类
add_test(NAME Protocol.Unit          COMMAND enterprise_tests --gtest_filter='Protocol*')
set_tests_properties(Protocol.Unit PROPERTIES LABELS "unit")

add_test(NAME WSReconnect.Integration COMMAND enterprise_tests --gtest_filter='Integration*WS*')
set_tests_properties(WSReconnect.Integration PROPERTIES LABELS "integration")

add_test(NAME Messages.E2E           COMMAND enterprise_tests --gtest_filter='E2E*Message*')
set_tests_properties(Messages.E2E PROPERTIES LABELS "e2e")

add_test(NAME MessageSend.Perf       COMMAND enterprise_tests --gtest_filter='Perf*Message*')
set_tests_properties(MessageSend.Perf PROPERTIES LABELS "performance")

7. 覆盖率目标

模块行覆盖率目标分支覆盖率目标说明
net/protocol90%85%纯函数,高覆盖率可实现
net/reconnect_strategy95%90%简单算法,边界值全覆盖
net/connection_mgr65%55%异步回调多,部分路径难触达
data/sync_mgr70%60%需 Mock 辅助
service/im_service70%60%核心业务逻辑
infra/config_mgr85%75%单例 + 线程安全
gui/*不强制GUI 用人工测试为主
整体核心库 (core/)≥ 70%≥ 60%

8. 测试数据管理

8.1 测试数据生成

// tests/common/test_data.h
namespace test {

// 生成可预测的测试消息
model::Message makeTestMessage(int64_t convId, int64_t senderId, int index) {
    model::Message msg;
    msg.msgId = fmt::format("test_msg_{:06d}", index);
    msg.convId = convId;
    msg.senderId = senderId;
    msg.senderName = fmt::format("User_{}", senderId);
    msg.contentType = 0;
    msg.contentBody = fmt::format(R"({{"type":"text","text":"test message {}"}})", index);
    msg.timestamp = 1704067200000 + index * 1000;
    msg.seq = index;
    return msg;
}

// 生成批量测试数据
std::vector<model::Message> makeTestMessages(int count, int64_t convId = 2001) {
    std::vector<model::Message> msgs;
    msgs.reserve(count);
    for (int i = 0; i < count; i++) {
        msgs.push_back(makeTestMessage(convId, 1002, i));
    }
    return msgs;
}

} // namespace test

8.2 Mock Server 场景文件管理

tests/scenarios/
├── normal_chat.json          # 正常聊天
├── offline_sync.json         # 离线消息同步 (200 条分批)
├── network_loss.json         # 网络丢包 + 重连
├── file_upload.json          # 文件上传 + 秒传
├── file_upload_chunk_loss.json # 分块上传丢包 (断点续传)
├── task_notify.json          # 任务通知推送
├── error_injection.json      # 综合错误注入
└── stress_1000msg.json       # 压力测试 (1000 条消息)

9. 缺陷管理约定

9.1 严重级别

级别定义响应时间
P0-致命崩溃、数据丢失、消息不可达立即修复
P1-严重核心功能不可用(无法登录、无法发送消息)24h 内
P2-一般非核心功能异常(统计图表错误、导出格式问题)本周内
P3-轻微UI 错位、文案错误、体验瑕疵下个迭代

9.2 测试报告模板

## 测试报告 — [版本号] — [日期]

### 概要
- 总用例数: 120
- 通过: 115
- 失败: 3
- 跳过: 2
- 通过率: 95.8%

### 失败用例
| 用例 | 级别 | 失败原因 | 关联 Issue |
|------|------|----------|-----------|
| ReconnectStrategy.DelayCaps | P2 | attempt 6 返回 30s 而非 60s | #142 |
| FileUpload.ChunkRetry     | P1 | 重试 3 次后未标记失败 | #143 |

### 覆盖率
- 行覆盖率: 72.3% (目标 70% ✅)
- 分支覆盖率: 61.1% (目标 60% ✅)

附录 A — 快速参考

命令用途
ctest --test-dir build -j4运行全部测试
ctest --test-dir build -L unit仅单元测试
ctest --test-dir build -R Protocol运行匹配 “Protocol” 的测试
./build/tests/enterprise_tests --gtest_list_tests列出所有测试
./build/tests/enterprise_tests --gtest_filter='*Reconnect*'运行过滤后的测试
./build/tests/enterprise_tests --gtest_repeat=10重复运行 10 次(排查偶发问题)

附录 B — 文档修订记录

版本日期作者变更说明
v0.12025-01初稿:测试金字塔、单元/集成/E2E/性能测试策略、CI 流水线、覆盖率目标