56、STL与测试框架:单元测试、性能测试、覆盖率分析
说实话,很多C++程序员写代码很猛,但一提到测试就头疼。我早年也这样——觉得测试是QA的事,自己只管把功能怼出来就行。直到有一次,一个看似完美的STL容器封装,上线后因为边界条件崩溃,差点背了P0事故。
从那以后,我养成了一个习惯:写STL代码的同时,必须把测试写好。今天咱们就聊聊,怎么用测试框架把STL代码的质量管起来。
为什么STL代码特别需要测试?
你想想看,STL本身就是个模板库,各种容器、算法、迭代器组合起来,复杂度是指数级上升的。我见过太多人这样写代码:
// 看起来很美的代码
std::vector<std::unique_ptr<Data>> vec;
vec.emplace_back(std::make_unique<Data>(...));
// 然后某个角落...
auto it = std::remove_if(vec.begin(), vec.end(), [](auto&& p) {
return p->isExpired();
});
// 忘了 erase? 恭喜你,内存泄漏 + 逻辑错误
这种问题,光靠肉眼review很难发现。但一个单元测试就能把它揪出来。
单元测试:给STL代码上保险
我个人习惯用 Google Test 做单元测试。为啥?因为它跟STL配合得特别好,尤其是那些匹配器(Matcher),简直是为STL容器量身定做的。
来看个实际例子。假设你写了一个函数,用来从vector中过滤出符合条件的元素:
// 被测试的函数
template<typename T, typename Pred>
std::vector<T> filter(const std::vector<T>& input, Pred pred) {
std::vector<T> result;
std::copy_if(input.begin(), input.end(), std::back_inserter(result), pred);
return result;
}
// 测试用例
TEST(FilterTest, HandlesEmptyVector) {
std::vector<int> empty;
auto result = filter(empty, [](int x) { return x > 0; });
EXPECT_TRUE(result.empty()); // 空输入必须返回空
}
TEST(FilterTest, FiltersCorrectly) {
std::vector<int> data = {1, -2, 3, -4, 5};
auto result = filter(data, [](int x) { return x > 0; });
// 用Google Test的容器匹配器
EXPECT_THAT(result, testing::ElementsAre(1, 3, 5));
EXPECT_EQ(result.size(), 3);
}
TEST(FilterTest, AllElementsMatch) {
std::vector<int> data = {1, 2, 3};
auto result = filter(data, [](int x) { return x > 0; });
EXPECT_EQ(result, data); // 全部匹配时,结果应该等于原容器
}
EXPECT_THAT 配合 ElementsAre、UnorderedElementsAre 这些匹配器。它们会给出非常清晰的失败信息,告诉你哪个元素不对。
性能测试:别让STL成为性能瓶颈
单元测试保证正确性,但性能问题呢?我曾经接手过一个项目,里面大量使用 std::list 做随机访问,结果性能惨不忍睹。这就是典型的「用错了容器」。
我推荐用 Google Benchmark 来做性能测试。它跟STL的配合也很默契:
#include <benchmark/benchmark.h>
// 对比 vector 和 list 的遍历性能
static void BM_VectorIteration(benchmark::State& state) {
std::vector<int> vec(state.range(0));
std::iota(vec.begin(), vec.end(), 0);
for (auto _ : state) {
volatile int sum = 0;
for (auto v : vec) {
sum += v;
}
}
}
static void BM_ListIteration(benchmark::State& state) {
std::list<int> lst(state.range(0));
std::iota(lst.begin(), lst.end(), 0);
for (auto _ : state) {
volatile int sum = 0;
for (auto v : lst) {
sum += v;
}
}
}
BENCHMARK(BM_VectorIteration)->Range(8, 8<<10);
BENCHMARK(BM_ListIteration)->Range(8, 8<<10);
跑出来的结果会让你大吃一惊——vector的遍历速度可能是list的10倍以上。这就是为什么我常说:选对容器,比优化代码更重要。
覆盖率分析:找到你没测到的代码
单元测试写了,性能测试也跑了,但你怎么知道测全了没有?覆盖率分析就是干这个的。
在Linux下,我一般用 gcov + lcov 的组合。用法很简单:
# 编译时加覆盖率标志
g++ -std=c++17 -fprofile-arcs -ftest-coverage -o test_program test.cpp
# 运行测试
./test_program
# 生成覆盖率报告
gcov test.cpp
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage_report
打开生成的HTML报告,你会看到每行代码被执行的次数。我一般关注三个指标:
| 指标 | 目标值 | 说明 |
|---|---|---|
| 行覆盖率 | > 90% | 每行代码至少被执行一次 |
| 分支覆盖率 | > 80% | if/else、switch 等分支都被覆盖 |
| 函数覆盖率 | 100% | 所有函数都应该被调用到 |
std::sort 在元素少于16个时会切换到插入排序,这个分支我的测试数据从来没触发过。从那以后,我写测试时会刻意构造不同规模的数据。
STL测试的完整流程
说了这么多,咱们来画个图,把整个流程串起来:
这个流程我用了好几年,效果不错。核心就是:单元测试保正确,性能测试保效率,覆盖率分析保全面。三者缺一不可。
实战建议
最后,分享几个我在项目中积累的经验:
- 测试数据要多样化:别只用小数据测。我一般会准备三组数据——小规模(10个元素)、中等规模(1000个)、大规模(10万个)。这样能发现不同规模下的性能问题。
- STL容器测试要覆盖所有操作:插入、删除、查找、遍历、排序……每个操作都要有对应的测试。特别是那些不常用的操作,比如
std::list::splice,很容易写错。 - 别忘了异常安全性:STL容器在内存分配失败时会抛异常。你的代码能正确处理吗?写个测试验证一下。
- 把测试集成到CI里:手动跑测试不靠谱。我习惯在GitHub Actions或Jenkins里配置好,每次push自动跑全套测试。
CTest + GTest 的组合。CMake原生支持Google Test,配置起来非常方便。只需要在CMakeLists.txt里加几行,就能自动发现并运行所有测试用例。
嗯,关于STL与测试框架,今天就聊到这儿。记住一句话:没有测试的STL代码,就像没有安全带的赛车——跑得快,但随时可能翻车。
公众号:蓝海资料掘金营,微信deep3321