乐于分享
好东西不私藏

Bazel C++ 构建系列文档(八):测试与持续集成

Bazel C++ 构建系列文档(八):测试与持续集成

1. Bazel 测试系统概览

Bazel 内置了强大的测试系统,支持多种测试类型和运行策略。

1.1 测试类型

测试类型
规则
特点
单元测试
cc_test
快速、隔离、专注单个组件
性能测试
cc_test

 + args=["--benchmark"]
测量性能指标
参数化测试
cc_test

 + args
同一测试用例不同参数
并发测试
cc_test

 + timeout
测试并发行为
集成测试
cc_test

 + data
测试多个组件交互

1.2 测试执行流程

测试执行流程:1. 测试发现 → bazel test //...2. 测试编译 → 编译测试目标3. 测试运行 → 在沙箱中执行测试4. 结果收集 → 收集输出和退出码5. 结果分析 → 判断通过/失败

2. 单元测试基础

2.1 基本 cc_test 规则

# tests/utils_test.cc#include "gtest/gtest.h"#include "utils.h"TEST(UtilsTest, SplitString) {  std::vector<std::string> result = utils::Split("a,b,c"',');  EXPECT_EQ(result.size(), 3);  EXPECT_EQ(result[0], "a");  EXPECT_EQ(result[1], "b");  EXPECT_EQ(result[2], "c");}TEST(UtilsTest, JoinString) {  std::vector<std::string> parts = {"hello""world"};  std::string result = utils::Join(parts, " ");  EXPECT_EQ(result, "hello world");}int main(int argc, char** argv) {  ::testing::InitGoogleTest(&argc, argv);  return RUN_ALL_TESTS();}
# tests/BUILDcc_test(    name = "utils_test",    srcs = ["utils_test.cc"],    deps = [        "//src:utils",        "@com_google_googletest//:gtest_main",    ],)

2.2 测试依赖管理

# 使用别名简化测试依赖cc_test(    name = "complex_test",    srcs = ["complex_test.cc"],    deps = [        ":complex_lib",        "@com_google_googletest//:gtest",        "@com_google_googletest//:gtest_main",        "@com_google_benchmark//:benchmark_main",    ],)

2.3 测试配置

# .bazelrctest --test_output=errors           # 只显示错误输出test --test_filter="*Test"         # 只运行 *Test 测试test --test_timeout=300,60,60      # 总/每个测试/每个动作超时test --test_summary=compact        # 简洁的测试摘要test --test_results_dir=test_logs  # 测试结果目录

3. Google Test 集成

3.1 Google Test 基本用法

// tests/example_test.cc#include"gtest/gtest.h"#include"example.h"// 正常测试TEST(ExampleTest, AddNumbers) {  EXPECT_EQ(example::Add(23), 5);  EXPECT_EQ(example::Add(-15), 4);}// 参数化测试class ExampleParamTest : public ::testing::TestWithParam<int> { protected:  void SetUp() override {    // 每个参数的设置  }};TEST_P(ExampleParamTest, MultiplyByTwo) {  int input = GetParam();  EXPECT_EQ(example::Multiply(input, 2), input * 2);}// 测试用例INSTANTIATE_TEST_SUITE_P(AllValues, ExampleParamTest,                         testing::Values(01234));

3.2 Google Mock 使用

// tests/mock_example_test.cc#include"gmock/gmock.h"#include"example.h"// 模拟类class MockExample : public example::ExampleInterface { public:  MOCK_METHOD(int, Add, (int a, int b), (override));};// 测试模拟类TEST(MockExampleTest, AddBehavior) {  MockExample mock;  EXPECT_CALL(mock, Add(23)).WillOnce(testing::Return(5));  EXPECT_CALL(mock, Add(-15)).WillOnce(testing::Return(4));  // 调用模拟方法  EXPECT_EQ(mock.Add(23), 5);  EXPECT_EQ(mock.Add(-15), 4);}

3.3 自定义测试夹具

// tests/fixture_test.cc#include"gtest/gtest.h"#include"database.h"class DatabaseTest : public ::testing::Test { protected:  void SetUp() override {    // 测试前的准备工作    db_ = std::make_unique<Database>("test.db");    db_->CreateTables();  }  void TearDown() override {    // 测试后的清理工作    if (db_) {      db_->Close();    }  }  std::unique_ptr<Database> db_;};TEST_F(DatabaseTest, InsertAndSelect) {  db_->InsertUser(1"Alice");  auto user = db->SelectUser(1);  ASSERT_NE(user, nullptr);  EXPECT_EQ(user->name(), "Alice");}// 参数化夹具class ParamDatabaseTest : public ::testing::TestWithParam<std::string> { protected:  void SetUp() override {    db_name_ = GetParam();    db_ = std::make_unique<Database>(db_name_);  }  std::string db_name_;  std::unique_ptr<Database> db_;};TEST_P(ParamDatabaseTest, TestAllImplementations) {  // 测试不同实现的数据库}

4. 测试运行控制

4.1 测试过滤

# 运行所有测试bazel test //...# 运行指定包的测试bazel test //tests:*# 运行特定测试bazel test //tests:utils_test# 使用通配符过滤bazel test //tests:*_test# 使用 -test_filter 参数bazel test //... --test_filter="*Test*"# 更复杂的过滤bazel test //... --test_filter="- flaky_test"  # 排除 flaky_test

4.2 测试标签

# tests/BUILDcc_test(    name = "unit_test",    srcs = ["unit_test.cc"],    tags = ["unit"],  # 单元测试标签)cc_test(    name = "integration_test",    srcs = ["integration_test.cc"],    tags = ["integration""slow"],  # 集成测试、慢速测试)cc_test(    name = "flaky_test",    srcs = ["flaky_test.cc"],    tags = ["flaky"],  # 不稳定测试)
# 按标签运行测试bazel test //... --test_tags_filters=unit      # 只运行单元测试bazel test //... --test_tags_filters=-slow     # 排除慢速测试bazel test //... --test_tags_filters=unit,integration  # 运行 unit 和 integration

4.3 并发测试控制

# .bazelrctest --test_timeout=300,60,60      # 总时间/每个测试/每个动作test --test_summary=terse          # 简洁输出test --test_output=errors         # 只显示错误test --local_test_jobs=8          # 本地并发数test --runs_per_test=3            # 每个测试运行次数# 测试大小分类test --test_size_filters=small    # 只运行小测试test --test_size_filters=medium   # 只运行中测试test --test_size_filters=large     # 只运行大测试

5. 性能测试(Benchmark)

5.1 Google Benchmark 集成

// tests/benchmark_test.cc#include<benchmark/benchmark.h>#include"utils.h"static void BM_StringConstruction(benchmark::State& state) {  for (auto _ : state) {    std::string s(1024, 'x');    benchmark::DoNotOptimize(s);  }}BENCHMARK(BM_StringConstruction);static void BM_VectorAppend(benchmark::State& state) {  for (auto _ : state) {    std::vector<int> v;    for (int i = 0; i < state.range(0); ++i) {      v.push_back(i);    }    benchmark::DoNotOptimize(v);  }}BENCHMARK(BM_VectorAppend)->Range(88<<10);static void BM_SortVector(benchmark::State& state) {  std::vector<int> data(10000);  std::iota(data.begin(), data.end(), 0);  std::random_shuffle(data.begin(), data.end());  for (auto _ : state) {    auto copy = data;    std::sort(copy.begin(), copy.end());    benchmark::DoNotOptimize(copy);  }}BENCHMARK(BM_SortVector);
# tests/BUILDcc_test(    name = "benchmark_test",    srcs = ["benchmark_test.cc"],    deps = [        "//src:utils",        "@com_google_benchmark//:benchmark_main",    ],    args = ["--benchmark_time_unit=ms"],  # 参数化)

5.2 运行性能测试

# 运行所有性能测试bazel test //tests:benchmark_test# 显示详细输出bazel test //tests:benchmark_test --test_output=all# 只运行特定基准测试bazel test //tests:benchmark_test --test_filter="BM_SortVector"# 使用 Google Benchmark 的选项bazel test //tests:benchmark_test -- --benchmark_repetitions=10bazel test //tests:benchmark_test -- --benchmark_time_unit=us

6. 测试数据文件

6.1 使用 data 属性

# tests/data_test.cc#include "gtest/gtest.h"#include <fstream>class DataTest : public ::testing::Test { protected:  void SetUp() override {    // 测试数据文件在 test_data 目录    data_file_ = testing::TempDir() + "/test_data.txt";    std::ofstream f(data_file_);    f << "test data";    f.close();  }  std::string data_file_;};TEST_F(DataTest, ReadTestData) {  std::ifstream f(data_file_);  std::string content((std::istreambuf_iterator<char>(f)),                       std::istreambuf_iterator<char>());  EXPECT_EQ(content, "test data");}
# tests/BUILDcc_test(    name = "data_test",    srcs = ["data_test.cc"],    data = [        "test_data/*.txt",        # 测试数据文件        "//data:test_config.json"# 其他包的数据文件    ],)

6.2 外部测试数据

# tests/BUILDcc_test(    name = "external_data_test",    srcs = ["external_data_test.cc"],    data = [        "@com_example_test_data//data:large_dataset.bin",    ],)# WORKSPACE 或 MODULE.bazelhttp_archive(    name = "com_example_test_data",    urls = ["https://example.com/test_data.zip"],    strip_prefix = "test_data",    build_file_content = """    package(default_visibility = ["//visibility:public"])    filegroup(name = "data", srcs glob(["**/*"]))    """,)

7. 测试沙箱与隔离

7.1 测试沙箱特性

Bazel 使用沙箱运行测试,确保测试隔离性和可重现性:

  • 每个测试在独立沙箱中运行
  • 测试不能修改源文件或构建产物
  • 测试只能访问 data 属性指定的文件
  • 环变量被限制在最小范围

7.2 自定义测试环境

# tests/BUILDcc_test(    name = "env_test",    srcs = ["env_test.cc"],    env_vars = {        "TEST_ENV_VAR""value",     # 设置环境变量        "PATH""/usr/bin:/bin",    # 自定义 PATH    },)

7.3 测试超时控制

# tests/BUILDcc_test(    name = "fast_test",    srcs = ["fast_test.cc"],    timeout = 5,  # 5 秒超时)cc_test(    name = "slow_test",    srcs = ["slow_test.cc"],    timeout = 300,  # 5 分钟超时    size = "large",  # 标记为大测试)

8. 测试结果分析

8.1 测试输出格式

# 详细的测试输出bazel test //... --test_output=all# 只失败的测试bazel test //... --test_output=errors# XML 格式输出bazel test //... --test_output=xml:test_results.xml# JSON 格式输出bazel test //... --test_output=json:test_results.json

8.2 测试结果目录

# 查看测试结果位置bazel info bazel-testlogs# 查看特定测试的日志cat bazel-testlogs/tests/utils_test/test.log# 查看失败的详细信息cat bazel-testlogs/tests/utils_test/test.failed

8.3 测试报告生成

# 测试报告脚本def generate_test_report():    import subprocess    import json    # 运行测试并收集结果    result = subprocess.run(        ["bazel""test""//...""--test_output=json"],        capture_output=True,        text=True    )    # 解析 JSON 结果    results = json.loads(result.stdout)    # 生成报告    report = {        "total"len(results["tests"]),        "passed"sum(1 for t in results["tests"if t["status"== "PASSED"),        "failed"sum(1 for t in results["tests"if t["status"== "FAILED"),        "flaky"sum(1 for t in results["tests"if t["status"== "FLAKY"),    }    return report

9. 持续集成集成

9.1 GitHub Actions 示例

# .github/workflows/bazel-test.ymlname: Bazel Testson: [push, pull_request]jobs:  test:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v3      - name: Install Bazel        uses: bazelbuild/setup-bazelisk@v1      - name: Cache Bazel output        uses: actions/cache@v2        with:          path'~/.cache/bazel'          key: {{ hashFiles('**/*.bzl''**/BUILD*', '**/*.bazel') }}      - name: Build        run: bazel build //...      - name: Run tests        run: bazel test //... --test_output=errors --local_test_jobs=8      - name: Run benchmarks        run: bazel test //tests:benchmark_test -- --benchmark_repetitions=5      - name: Upload test results        uses: actions/upload-artifact@v2        if: always()        with:          name: test-results          path: bazel-testlogs/

9.2 CI 中的测试优化

# CI 中常用的测试命令bazel test //... --test_timeout=600,120,60  # 防止测试超时bazel test //... --test_summary=terse      # 减少输出bazel test //... --runs_per_test=1         # CI 中只运行一次bazel test //... --flaky_test_attempts=3   # 重试失败测试# 只运行关键测试bazel test //tests:unit --test_size_filters=smallbazel test //tests:integration --test_size_filters=medium

9.3 测试监控

# 测试监控脚本import subprocessimport jsonimport sysdef monitor_tests():    # 监控测试执行    result = subprocess.run([        "bazel""test""//..."        "--test_output=json",        "--local_test_jobs=4"    ], capture_output=True, text=True)    # 分析测试结果    data = json.loads(result.stdout)    # 检查失败测试    failed_tests = [t for t in data["tests"if t["status"== "FAILED"]    if failed_tests:        print(f"❌ {len(failed_tests)} tests failed:")        for test in failed_tests:            print(f"  - {test['name']}")        sys.exit(1)    else:        print(f"✅ All {len(data['tests'])} tests passed")

10. 测试最佳实践

10.1 测试组织原则

✅ 推荐做法                          ❌ 避免─────────────────────────          ─────────────────────────每个测试专注单一职责                在一个测试中测试多个功能使用有意义的测试名称               使用无意义的命名如 test1测试应该是独立的                    测试之间有依赖测试应该快速运行                   包含耗时操作如网络请求使用模拟对象                        依赖外部服务测试覆盖率 >80%                     不写关键测试的测试

10.2 测试命名约定

// 好的命名TEST(UtilsTest, SplitStringByComma)     // 类名 + 功能描述TEST(ParserTest, InvalidInputReturnsError)TEST(DatabaseTest, InsertDuplicateUserReturnsError)// 避免的命名TEST(T1)                              // 无意义TEST(TestSomething)                   // 太泛TEST(utils)                           // 缺少功能描述

10.3 测试数据管理

tests/├── BUILD├── unit/                          # 单元测试│   ├── BUILD│   └── ...├── integration/                   # 集成测试│   ├── BUILD│   └── test_data/│       ├── valid_input.json│       └── error_cases.json├── performance/                   # 性能测试│   ├── BUILD│   └── datasets/│       └── large_dataset.bin└── fixtures/                      # 测试夹具    └── mock_services.h

11. 小结

本篇详细介绍了 Bazel 的测试系统:

  • ✅ cc_test 规则和测试类型
  • ✅ Google Test/Google Mock 集成
  • ✅ 测试运行控制和过滤
  • ✅ 性能测试(Benchmark)
  • ✅ 测试数据文件管理
  • ✅ 测试沙箱与隔离
  • ✅ 测试结果分析
  • ✅ CI/CD 集成
  • ✅ 测试最佳实践