写 C++ 久了就会发现,测试和调试不是附属品,而是开发节奏的一部分。测试帮你把预期行为固定下来,重构时更有底气;调试则是在代码跑偏时,把真相从变量和调用栈里挖出来。这里整理一些个人常用的套路,不追求大而全,但求上手能打。
测试的分层与原则
测试通常按粒度分层,每层目标不同:
- 单元测试:盯单个函数或类,隔离外部依赖。
- 集成测试:验证模块间交互,像 Calculator 和 Parser 能不能对上暗号。
- 系统测试:端到端模拟真实环境,保证整个应用行为正常。
- 验收测试:确认产物符合需求,这一步经常由 QA 主导。
- 性能测试:压一压瓶颈,看看响应时间和资源占用。
无论哪一层,几条原则值得反复念叨:测试要早写,别等到提测前才补;用例要独立,不能依赖执行顺序;尽量自动化,否则没人愿意跑;结果要稳定,不是概率游戏。
单元测试框架
C++ 的单元测试框架多到选择困难,但 Google Test 和 Catch2 基本覆盖了大部分场景。Google Test 功能全、社区大,Catch2 语法轻、单头文件就能用。我一般看项目环境—如果已经有 CMake 和 Google Test 集成,就用前者;如果是个小工具,Catch2 拖进去就写。
Google Test
安装不复杂:
# Ubuntu/Debian
sudo apt-get install libgtest-dev
# macOS (Homebrew)
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.setValue(10);
int result = obj.add(20);
EXPECT_EQ(result, 30);
EXPECT_EQ(obj.getValue(), 30);
}
TEST(MyClassTest, SubtractTest) {
MyClass obj;
obj.setValue(50);
int result = obj.subtract(20);
EXPECT_EQ(result, 30);
EXPECT_EQ(obj.getValue(), 30);
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Catch2
Catch2 的 TEST_CASE 更灵活,可以直接打标签方便分组。安装同样简单:
# Ubuntu/Debian
sudo apt-get install catch2
# macOS (Homebrew)
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,否则符号信息进不来。
# 编译并进入 GDB
g++ -g program.cpp -o program
gdb program
# 几个常用命令
break main # 在 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
头文件与实现
Calculator.h
#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 // CALCULATOR_H
Calculator.cpp
#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;
}
Parser.h
#ifndef PARSER_H
#define PARSER_H
#include <string>
#include "Calculator.h"
class Parser {
public:
int parse(const std::string& expression);
};
#endif // PARSER_H
Parser.cpp
#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("无效的运算符");
}
}
main.cpp
#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;
}
测试代码
CalculatorTest.cpp
#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);
}
ParserTest.cpp
#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);
}
IntegrationTest.cpp
#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);
}
构建与运行
根目录 CMakeLists.txt:
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)
tests/CMakeLists.txt:
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 自动跑,才能真正把质量嵌进开发流程里。

