跳到主要内容
C++ 测试与调试:从框架到工具链的实践备忘 | 极客日志
C++
C++ 测试与调试:从框架到工具链的实践备忘 从单元测试到集成调试的实战手册,覆盖 Google Test 与 Catch2 的写法、GDB 和 Visual Studio 调试器的常用操作,并通过一个计算器项目展示如何组织测试、构建与运行。同时给出 Mock、性能分析、静态检查和 CI 集成的进阶方向。
月亮邮递员 发布于 2026/6/27 更新于 2026/7/2 2 浏览
写 C++ 久了就会发现,测试和调试不是附属品,而是开发节奏的一部分。测试帮你把预期行为固定下来,重构时更有底气;调试则是在代码跑偏时,把真相从变量和调用栈里挖出来。这里整理一些个人常用的套路,不追求大而全,但求上手能打。
测试的分层与原则
测试通常按粒度分层,每层目标不同:
单元测试 :盯单个函数或类,隔离外部依赖。
集成测试 :验证模块间交互,像 Calculator 和 Parser 能不能对上暗号。
系统测试 :端到端模拟真实环境,保证整个应用行为正常。
验收测试 :确认产物符合需求,这一步经常由 QA 主导。
性能测试 :压一压瓶颈,看看响应时间和资源占用。
无论哪一层,几条原则值得反复念叨:测试要早写 ,别等到提测前才补;用例要独立 ,不能依赖执行顺序;尽量自动化 ,否则没人愿意跑;结果要稳定 ,不是概率游戏。
单元测试框架
C++ 的单元测试框架多到选择困难,但 Google Test 和 Catch2 基本覆盖了大部分场景。Google Test 功能全、社区大,Catch2 语法轻、单头文件就能用。我一般看项目环境—如果已经有 CMake 和 Google Test 集成,就用前者;如果是个小工具,Catch2 拖进去就写。
Google Test
安装不复杂:
sudo apt-get install libgtest-dev
brew install googletest
典型写法如下,断言宏丰富,EXPECT_* 失败不中止当前用例,ASSERT_* 则直接停。
#include <gtest/gtest.h>
#include "MyClass.h"
TEST (MyClassTest, ConstructorTest) {
MyClass obj;
EXPECT_EQ (obj.getValue (), 0 );
}
TEST (MyClassTest, SetGetValueTest) {
MyClass obj;
obj.setValue (42 );
EXPECT_EQ (obj.getValue (), 42 );
}
TEST (MyClassTest, AddTest) {
MyClass obj;
obj. ( );
result = obj. ( );
(result, );
(obj. (), );
}
(MyClassTest, SubtractTest) {
MyClass obj;
obj. ( );
result = obj. ( );
(result, );
(obj. (), );
}
{
::testing:: (&argc, argv);
();
}
setValue
10
int
add
20
EXPECT_EQ
30
EXPECT_EQ
getValue
30
TEST
setValue
50
int
subtract
20
EXPECT_EQ
30
EXPECT_EQ
getValue
30
int main (int argc, char ** argv)
InitGoogleTest
return
RUN_ALL_TESTS
Catch2 Catch2 的 TEST_CASE 更灵活,可以直接打标签方便分组。安装同样简单:
sudo apt-get install catch2
brew install catch2
#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>
#include "MyClass.h"
TEST_CASE ("MyClass Constructor Test" , "[constructor]" ) {
MyClass obj;
REQUIRE (obj.getValue () == 0 );
}
TEST_CASE ("MyClass Set and Get Value Test" , "[setget]" ) {
MyClass obj;
obj.setValue (42 );
REQUIRE (obj.getValue () == 42 );
}
TEST_CASE ("MyClass Add Test" , "[add]" ) {
MyClass obj;
obj.setValue (10 );
int result = obj.add (20 );
REQUIRE (result == 30 );
REQUIRE (obj.getValue () == 30 );
}
TEST_CASE ("MyClass Subtract Test" , "[subtract]" ) {
MyClass obj;
obj.setValue (50 );
int result = obj.subtract (20 );
REQUIRE (result == 30 );
REQUIRE (obj.getValue () == 30 );
}
调试工具 调试器的选择跟着操作系统走。Linux 上用 GDB,命令行虽然有时劝退,但必备命令背熟之后效率很高;Windows 下 Visual Studio 的图形化调试体验更友好,断点拖拽、变量监视都很直观。
GDB 速查
g++ -g program.cpp -o program
gdb program
break main
run
next
step
print variable
watch variable
backtrace
continue
quit
调试时可以在关键位置打上 break,print 变量看看是不是预期值。比如下面这个简单的程序:
#include <iostream>
#include "MyClass.h"
int main () {
MyClass obj;
obj.setValue (42 );
int result = obj.add (20 );
std::cout << "结果:" << result << std::endl;
return 0 ;
}
跑起来后在 setValue 后面停下,看一眼 obj.getValue(),确认 42 进去了再继续。
Visual Studio 调试器 在行号左侧点一下断点,F5 开始调试,程序停住后就能在局部变量窗口和调用堆栈里探索。F10 / F11 分别对应逐过程、逐语句,和 GDB 的 next/step 差不多。比较复杂的多线程程序里,用 VS 的并行线程窗口去追数据竞争会省很多时间。
集成测试 单元测试只保证零件没问题,集成测试才看零件组装后会不会互相扯皮。常见的策略有自顶向下、自底向上,或者三明治混合。实际项目中很少死守一种,通常哪种能先跑通就用哪种。
下面是一个简单的集成测试示例,把 Calculator 和 Parser 绑在一起:
#include <gtest/gtest.h>
#include "Calculator.h"
#include "Parser.h"
TEST (IntegrationTest, CalculateTest) {
Calculator calculator;
Parser parser;
std::string expression = "10 + 20" ;
int result = parser.parse (expression);
EXPECT_EQ (result, 30 );
expression = "10 * 20" ;
result = parser.parse (expression);
EXPECT_EQ (result, 200 );
expression = "(10 + 20) * 3" ;
result = parser.parse (expression);
EXPECT_EQ (result, 90 );
}
注意这个集成测试直接用 Google Test 来写,也可以放到 Catch2 里,看团队统一选哪个。
综合案例:带测试的计算器 下面用一个简单的计算器项目把测试、调试串起来。项目结构尽量保持清晰:
CalculatorProject/
├── include/
│ ├── Calculator.h
│ └── Parser.h
├── src /
│ ├── Calculator.cpp
│ ├── Parser.cpp
│ └── main .cpp
├── tests/
│ ├── CalculatorTest.cpp
│ ├── ParserTest.cpp
│ └── IntegrationTest.cpp
└── CMakeLists.txt
头文件与实现 #ifndef CALCULATOR_H
#define CALCULATOR_H
class Calculator {
public :
static int add (int a, int b) ;
static int subtract (int a, int b) ;
static int multiply (int a, int b) ;
static int divide (int a, int b) ;
};
#endif
#include "Calculator.h"
#include <stdexcept>
int Calculator::add (int a, int b) {
return a + b;
}
int Calculator::subtract (int a, int b) {
return a - b;
}
int Calculator::multiply (int a, int b) {
return a * b;
}
int Calculator::divide (int a, int b) {
if (b == 0 ) {
throw std::invalid_argument ("除数不能为零" );
}
return a / b;
}
#ifndef PARSER_H
#define PARSER_H
#include <string>
#include "Calculator.h"
class Parser {
public :
int parse (const std::string& expression) ;
};
#endif
#include "Parser.h"
#include <sstream>
int Parser::parse (const std::string& expression) {
std::istringstream iss (expression) ;
int a, b;
char op;
iss >> a >> op >> b;
switch (op) {
case '+' : return Calculator::add (a, b);
case '-' : return Calculator::subtract (a, b);
case '*' : return Calculator::multiply (a, b);
case '/' : return Calculator::divide (a, b);
default : throw std::invalid_argument ("无效的运算符" );
}
}
#include <iostream>
#include "Parser.h"
int main () {
Parser parser;
try {
std::string expression;
std::cout << "请输入表达式(例如:10 + 20): " ;
std::getline (std::cin, expression);
int result = parser.parse (expression);
std::cout << "结果:" << result << std::endl;
} catch (const std::exception& e) {
std::cerr << "错误:" << e.what () << std::endl;
return 1 ;
}
return 0 ;
}
测试代码 #include <gtest/gtest.h>
#include "Calculator.h"
TEST (CalculatorTest, AddTest) {
EXPECT_EQ (Calculator::add (10 , 20 ), 30 );
EXPECT_EQ (Calculator::add (-10 , 20 ), 10 );
EXPECT_EQ (Calculator::add (0 , 0 ), 0 );
}
TEST (CalculatorTest, SubtractTest) {
EXPECT_EQ (Calculator::subtract (20 , 10 ), 10 );
EXPECT_EQ (Calculator::subtract (10 , 20 ), -10 );
EXPECT_EQ (Calculator::subtract (0 , 0 ), 0 );
}
TEST (CalculatorTest, MultiplyTest) {
EXPECT_EQ (Calculator::multiply (10 , 20 ), 200 );
EXPECT_EQ (Calculator::multiply (-10 , 20 ), -200 );
EXPECT_EQ (Calculator::multiply (0 , 10 ), 0 );
}
TEST (CalculatorTest, DivideTest) {
EXPECT_EQ (Calculator::divide (20 , 10 ), 2 );
EXPECT_EQ (Calculator::divide (-20 , 10 ), -2 );
EXPECT_THROW (Calculator::divide (10 , 0 ), std::invalid_argument);
}
#include <gtest/gtest.h>
#include "Parser.h"
TEST (ParserTest, ParseTest) {
Parser parser;
EXPECT_EQ (parser.parse ("10 + 20" ), 30 );
EXPECT_EQ (parser.parse ("20 - 10" ), 10 );
EXPECT_EQ (parser.parse ("10 * 20" ), 200 );
EXPECT_EQ (parser.parse ("20 / 10" ), 2 );
EXPECT_THROW (parser.parse ("10 / 0" ), std::invalid_argument);
EXPECT_THROW (parser.parse ("10 a 20" ), std::invalid_argument);
}
#include <gtest/gtest.h>
#include "Parser.h"
TEST (IntegrationTest, CalculateTest) {
Parser parser;
EXPECT_EQ (parser.parse ("10 + 20" ), 30 );
EXPECT_EQ (parser.parse ("20 - 10" ), 10 );
EXPECT_EQ (parser.parse ("10 * 20" ), 200 );
EXPECT_EQ (parser.parse ("20 / 10" ), 2 );
EXPECT_THROW (parser.parse ("10 / 0" ), std::invalid_argument);
}
构建与运行 cmake_minimum_required(VERSION 3.10)
project(CalculatorProject)
set(CMAKE_CXX_STANDARD 17)
include_directories(include)
add_library(Calculator src/Calculator.cpp)
add_library(Parser src/Parser.cpp)
add_executable(CalculatorApp src/main.cpp)
target_link_libraries(CalculatorApp Calculator Parser)
add_subdirectory(tests)
cmake_minimum_required(VERSION 3.10)
find_package(GTest REQUIRED)
include_directories(${GTEST_INCLUDE_DIRS})
add_executable(CalculatorTests CalculatorTest.cpp ParserTest.cpp IntegrationTest.cpp)
target_link_libraries(CalculatorTests ${GTEST_LIBRARIES} Calculator Parser pthread)
include(GoogleTest)
gtest_discover_tests(CalculatorTests)
mkdir -p build && cd build
cmake ..
make
ctest
如果一切正常,会看到所有测试通过。这时候改完代码就能随手跑一遍测试,心里不慌。
下一步 测试与调试的实践远不止这些。如果项目里有大量外部依赖,可以试试 Google Mock 做模拟对象;关心性能就用 Google Benchmark 配合 gcov 看覆盖率;静态检查用 Clang Static Analyzer 或 clang-tidy 提前扫出潜在问题;最后把测试接入 GitHub Actions 等 CI 平台,每次 push 自动跑,才能真正把质量嵌进开发流程里。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online