06 | 200个单元测试C项目也能TDD

本文目录

  • [🧪 第 6 篇:200 个单元测试 ------ C 项目也能 TDD](#🧪 第 6 篇:200 个单元测试 —— C 项目也能 TDD)
    • [📌 1. 架构:C + C++ 混合编译](#📌 1. 架构:C + C++ 混合编译)
      • [1.1 为什么是 Google Test?](#1.1 为什么是 Google Test?)
      • [1.2 构建魔法](#1.2 构建魔法)
      • [1.3 为什么 main.c 不能直接测?](#1.3 为什么 main.c 不能直接测?)
    • [📂 2. 测试分层](#📂 2. 测试分层)
      • [2.1 test_game_logic:算法完整性](#2.1 test_game_logic:算法完整性)
      • [2.2 test_fixes:回归防护网](#2.2 test_fixes:回归防护网)
    • [🎯 3. 精选测试用例](#🎯 3. 精选测试用例)
      • [3.1 浮点边界:NaN / Inf / 1e300](#3.1 浮点边界:NaN / Inf / 1e300)
      • [3.2 整数边界:有符号溢出 UB](#3.2 整数边界:有符号溢出 UB)
      • [3.3 RenderTexture 缩放矩阵(24 个子测试)](#3.3 RenderTexture 缩放矩阵(24 个子测试))
    • [📈 4. 测试数的演化](#📈 4. 测试数的演化)
    • [🚫 5. 测试的边界](#🚫 5. 测试的边界)
    • [💭 6. 给你的建议](#💭 6. 给你的建议)
    • [🔜 下篇预告(最终篇)](#🔜 下篇预告(最终篇))
    • [📚 系列目录](#📚 系列目录)

🎯 系列第 6 篇 · 有人说"C 写测试太麻烦" ------ 200 个用例告诉你,C 项目也可以 TDD。

📖 上一篇持久化、撤销、提示:非核心功能


🧪 第 6 篇:200 个单元测试 ------ C 项目也能 TDD


📌 1. 架构:C + C++ 混合编译

1.1 为什么是 Google Test?

方案 断言丰富度 维护状态 Windows 兼容
Google Test 🟢 丰富 🟢 活跃 🟢 好
❌ Unity 🟡 基础 🟡 一般 🟢 好
❌ CMocka 🟡 一般 🔴 缓慢 🟡 一般
❌ Criterion 🟡 一般 🟡 一般 🟡 一般

Google Test 的致命优势:参数化测试 + 丰富的断言宏 + 活跃社区。

1.2 构建魔法

复制代码
g++ -std=c++17 -O2 -Wall \
  -I. -Ithird_party/googletest/include \
  tests/test_game_logic.cpp \
  src/game_logic.c \          ← C 源码直接编译!
  -L. -lgtest -lgtest_main \
  -o tests/test_game_logic.exe

#mermaid-svg-9E6zOn00I6YgQaJJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9E6zOn00I6YgQaJJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9E6zOn00I6YgQaJJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9E6zOn00I6YgQaJJ .error-icon{fill:#552222;}#mermaid-svg-9E6zOn00I6YgQaJJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9E6zOn00I6YgQaJJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9E6zOn00I6YgQaJJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9E6zOn00I6YgQaJJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9E6zOn00I6YgQaJJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9E6zOn00I6YgQaJJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9E6zOn00I6YgQaJJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9E6zOn00I6YgQaJJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9E6zOn00I6YgQaJJ .marker.cross{stroke:#333333;}#mermaid-svg-9E6zOn00I6YgQaJJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9E6zOn00I6YgQaJJ p{margin:0;}#mermaid-svg-9E6zOn00I6YgQaJJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9E6zOn00I6YgQaJJ .cluster-label text{fill:#333;}#mermaid-svg-9E6zOn00I6YgQaJJ .cluster-label span{color:#333;}#mermaid-svg-9E6zOn00I6YgQaJJ .cluster-label span p{background-color:transparent;}#mermaid-svg-9E6zOn00I6YgQaJJ .label text,#mermaid-svg-9E6zOn00I6YgQaJJ span{fill:#333;color:#333;}#mermaid-svg-9E6zOn00I6YgQaJJ .node rect,#mermaid-svg-9E6zOn00I6YgQaJJ .node circle,#mermaid-svg-9E6zOn00I6YgQaJJ .node ellipse,#mermaid-svg-9E6zOn00I6YgQaJJ .node polygon,#mermaid-svg-9E6zOn00I6YgQaJJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9E6zOn00I6YgQaJJ .rough-node .label text,#mermaid-svg-9E6zOn00I6YgQaJJ .node .label text,#mermaid-svg-9E6zOn00I6YgQaJJ .image-shape .label,#mermaid-svg-9E6zOn00I6YgQaJJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-9E6zOn00I6YgQaJJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9E6zOn00I6YgQaJJ .rough-node .label,#mermaid-svg-9E6zOn00I6YgQaJJ .node .label,#mermaid-svg-9E6zOn00I6YgQaJJ .image-shape .label,#mermaid-svg-9E6zOn00I6YgQaJJ .icon-shape .label{text-align:center;}#mermaid-svg-9E6zOn00I6YgQaJJ .node.clickable{cursor:pointer;}#mermaid-svg-9E6zOn00I6YgQaJJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9E6zOn00I6YgQaJJ .arrowheadPath{fill:#333333;}#mermaid-svg-9E6zOn00I6YgQaJJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9E6zOn00I6YgQaJJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9E6zOn00I6YgQaJJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9E6zOn00I6YgQaJJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9E6zOn00I6YgQaJJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9E6zOn00I6YgQaJJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9E6zOn00I6YgQaJJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9E6zOn00I6YgQaJJ .cluster text{fill:#333;}#mermaid-svg-9E6zOn00I6YgQaJJ .cluster span{color:#333;}#mermaid-svg-9E6zOn00I6YgQaJJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9E6zOn00I6YgQaJJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9E6zOn00I6YgQaJJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-9E6zOn00I6YgQaJJ .icon-shape,#mermaid-svg-9E6zOn00I6YgQaJJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9E6zOn00I6YgQaJJ .icon-shape p,#mermaid-svg-9E6zOn00I6YgQaJJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9E6zOn00I6YgQaJJ .icon-shape .label rect,#mermaid-svg-9E6zOn00I6YgQaJJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9E6zOn00I6YgQaJJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9E6zOn00I6YgQaJJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9E6zOn00I6YgQaJJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} main.c

含 raylib

不可测
game_logic.c

纯逻辑

可测
Google Test

C++ 测试框架
200 个测试用例

全部绿色通过

关键game_logic.c 直接用 g++ 编译。C++ 编译器对 C 代码兼容性足够好(只要代码写得规范)。

1.3 为什么 main.c 不能直接测?

main.c 链接了 raylib 图形库,需要 OpenGL 上下文。在命令行/CI 环境中无法运行。

解决方案 :将 PlaceMinesRevealCellCalculateAdjacentCheckWinUpdateStats 等不依赖图形的函数提取到 game_logic.cmain.c 通过 #include "game_logic.h" 调用。测试只编译 game_logic.c


📂 2. 测试分层

复制代码
tests/
├── test_game_logic.cpp    # 46 个算法测试
│                          #   布雷、展开、和弦、胜利检测
│
└── test_fixes.cpp         # 154 个回归测试
                           #   每个 Fix 对应一组用例
                           #   Fix99: 8 个 ESC 状态矩阵
                           #   Fix108: 24 个缩放比例
                           #   Fix82: 12 个 ValidateStats

2.1 test_game_logic:算法完整性

cpp 复制代码
TEST(RevealCellTest, RevealZeroAdjacentFloodFills) {
    Game g;
    InitNewGame(&g, DIFFICULTY_BEGINNER);
    // 手动设置棋盘,让 (0,0) 为 0 雷格
    RevealCell(&g, 0, 0);
    EXPECT_TRUE(g.board[0][0].isRevealed);
    EXPECT_TRUE(g.board[0][1].isRevealed);    // 被泛洪展开
    EXPECT_TRUE(g.board[1][0].isRevealed);
    EXPECT_FALSE(g.board[3][3].isRevealed);   // 远处的没被展开
}

2.2 test_fixes:回归防护网

cpp 复制代码
// Fix #99 ESC 八状态矩阵
TEST_F(Fix99EscapeKeyTest, MenuStateExitsGame) {
    game.state = GL_STATE_MENU;
    EXPECT_EQ(GL_HandleEscape(&game), GL_ACTION_QUIT);
}

TEST_F(Fix99EscapeKeyTest, AllNonMenuStatesReturnMenu) {
    GL_GameState nonMenuStates[] = {
        GL_STATE_PLAYING, GL_STATE_WIN, GL_STATE_LOSE,
        GL_STATE_STATS, GL_STATE_CUSTOM
    };
    for (auto s : nonMenuStates) {
        game.state = s;
        EXPECT_EQ(GL_HandleEscape(&game), GL_ACTION_MENU);
    }
}

🎯 命名规范Fix{N}{描述}Test。一眼看出这是哪个修复的回归测试。


🎯 3. 精选测试用例

3.1 浮点边界:NaN / Inf / 1e300

cpp 复制代码
TEST(ValidateStatsTest, TotalTimeMaxBoundaryReset) {
    GL_Stats stats = {};
    stats.totalTime[0] = 1e300;       // 极端大值
    GL_ValidateStats(&stats);
    EXPECT_DOUBLE_EQ(stats.totalTime[0], 0.0);  // 被重置
}

TEST(ValidateStatsTest, TotalTimeNaNReset) {
    GL_Stats stats = {};
    stats.totalTime[0] = nan("");               // NaN
    GL_ValidateStats(&stats);
    EXPECT_DOUBLE_EQ(stats.totalTime[0], 0.0);
}

💡 这些不是"万一发生"的场景 ------ 文件损坏、手动编辑、跨平台复制,都会产生异常值。不测就是赌。

3.2 整数边界:有符号溢出 UB

cpp 复制代码
TEST(Fix87IntegerOverflowTest, MaximumBoardSize) {
    unsigned rows = 16, cols = 30;
    int safeMines = (int)((unsigned)rows * (unsigned)cols) - 9;
    EXPECT_EQ(safeMines, 471);  // 480 - 9
}

C 标准:有符号整数溢出 = undefined behavior 。用 (unsigned) 强制转换消除风险。测试验证计算结果正确。

3.3 RenderTexture 缩放矩阵(24 个子测试)

cpp 复制代码
TEST_F(Fix108RenderTextureScalingTest, PreciseMatch) {
    UpdateCanvasScale(1280, 720);            // 精确匹配
    EXPECT_FLOAT_EQ(canvasScale, 1.0f);
    EXPECT_FLOAT_EQ(canvasOffsetX, 0.0f);
}

TEST_F(Fix108RenderTextureScalingTest, WideScreen) {
    UpdateCanvasScale(1600, 720);            // 宽屏
    EXPECT_GT(canvasOffsetX, 0.0f);          // 水平居中偏移
    EXPECT_FLOAT_EQ(canvasOffsetY, 0.0f);    // 垂直无偏移
}

覆盖 24 种窗口/画布比例组合:精确匹配、2 倍缩放、宽屏、窄屏、超宽屏、HD + 居中...... 确保 canvasScale 计算在任何窗口尺寸下都正确。

窗口尺寸 画布尺寸 canvasScale canvasOffsetX canvasOffsetY 测试结果
1280×720 1280×720 1.0 0.0 0.0 ✅ 精确匹配
1600×720 1280×720 0.5625 160.0 0.0 ✅ 宽屏
960×720 1280×720 0.5625 0.0 0.0 ✅ 窄屏
2560×720 1280×720 0.5625 640.0 0.0 ✅ 超宽屏
1920×1080 1280×720 1.5 0.0 0.0 ✅ HD 全屏
3840×2160 1280×720 3.0 0.0 0.0 ✅ 4K 全屏
1280×960 1280×720 0.75 0.0 120.0 ✅ 竖屏
1280×480 1280×720 0.6667 0.0 0.0 ✅ 窄高屏

📈 4. 测试数的演化

复制代码
Round  测试数    里程碑
─────  ─────    ────────────────────────────
init      0    项目开始 ------ 没有测试 ❌
R2-3     46    添加 game_logic 算法测试
R6-13    47    每次 Fix 加一个回归测试
R14      93    浮点/校验和保护测试
R15     100    ESC/坐标/布局回归
R18     146    RenderTexture 缩放矩阵
R24     200    🎉 综合功能测试完成

200 个用例、全部绿色通过,已经成为重构的"安全网"。


🚫 5. 测试的边界

单元测试再好,也测不了:

项目 原因 替代验证
图形渲染颜色 无 OpenGL 上下文 人工目测
按钮与文字重叠 布局不在 game_logic 启动冒烟测试
帧时序/动画 测试不跑帧循环 人工核查
文件 I/O 权限 无实际文件系统隔离 手工测试

🔑 200 个自动化测试 + 启动冒烟测试(基本能运行) + 手工 GUI 验证清单,才是完整方案。


💭 6. 给你的建议

c 复制代码
if (项目行数 > 1000) {
    write_tests();  // 强烈建议
}
if (项目有文件 I/O || 浮点运算) {
    boundary_tests(); // 必须!NaN/Inf/INT_MAX 一定会来
}
if (项目有状态机) {
    state_matrix_tests(); // 每个状态 × 每个输入
}

🎯 从第 1 个 Fix 就开始写回归测试。 不写的话,下次重构你不敢改;写了,你就有信心。


🔜 下篇预告(最终篇)

第 7 篇:960×640 → 1280×720:一次全局缩放重构实录

CELL_SIZE 从 30 到 36 到 40 的迭代决策、菜单/统计/自定义三屏同步布局、HUD 重叠的 5 次修复全过程。


📚 系列目录

# 标题 状态
1 项目概览与扫雷核心算法 ✅ 已发布
2 Raylib 渲染架构 ✅ 已发布
3 222 个 Bug 修复教会我的事 ✅ 已发布
4 中英文双语的工程实现 ✅ 已发布
5 持久化、撤销、提示 ✅ 已发布
6 200 个单元测试 ← 本文 ✅ 已发布
7 全局缩放重构实录 📝 待发布

👍 如果你觉得有帮助,点赞 + 收藏 + 关注,三连支持作者继续写下去 🚀

相关推荐
●VON9 小时前
HarmonyKit | 鸿蒙开发展望:HarmonyKit 未来路线图与社区共建
华为·单元测试·harmonyos·鸿蒙
烟锁池塘柳011 小时前
【C/C++】解决C++控制台输出中文乱码问题
c语言·开发语言·c++
捷瑞电子工坊13 小时前
C语言联合体(Union)——让同一块内存“变脸”
c语言·union·联合体·内存共享
一叶飘零_sweeeet14 小时前
AI 编程时代的质量底座:SDD 规范驱动与 TDD 测试驱动深度实战
人工智能·ai编程·tdd·sdd
●VON15 小时前
HarmonyKit | 鸿蒙新特性:HDS 沉浸光感与空间化视觉设计
华为·性能优化·单元测试·harmonyos·鸿蒙
aaPIXa62217 小时前
C++ 用 Claude Code 的 defending-code-reference-harness:C/C++ 内存漏洞自动发现与修复 Pipeline
java·c语言·c++
xingke1 天前
C 语言多文件编程:(一)头文件、extern、编译链接
c语言·开发语言·多文件
wuyk5551 天前
38.什么是C语言?从语法到工程落地的工程师视角解读
c语言·开发语言·stm32·单片机·mcu·51单片机
程序员天天困1 天前
JaCoCo 代码覆盖率:Spring Boot 集成与报告解读
单元测试·测试