chapter08 - testing

CTest

基本用法

要使用 CTest 和相关的东西需要调用 enable_testing

ctest 是要在 build tree 执行的。

ctest 也能直接编译和运行(两步合为一步),但是要手动提供测试命令(有点奇怪)。

Dry run:

CTest offers an -N option, which disables execution and only prints a list.

将测试用标签分组(之前我在实验室项目里面只用了 NAME 关键字):

CTest also offers a mechanism to group tests with LABELS keyword.

也可以在测试添加完成之后再通过更改属性改变 LABELS:

set_tests_properties(<name> PROPERTIES LABELS "<label>")

从单词复数形式来看应该是能够添加多个标签。

过滤测试可以用正则表达式对 label 或者 name 匹配、反向匹配

--schedule-random 可以打乱顺序测试,提高测试的稳定性,避免前一个测试干扰后一个测试。

–-repeat 选项能够重复测试,直到测试成功、失败、超时或者达到最大测试数量。

-j 可以指定并发测试,这样测试速度更快。

-C 或者 --build-config 可以指定要测试的 config(对 multi-config generator 有效)。

--test-load 控制 CPU 的占用。

--timeout 指定单个测试的超时时间。

一般的使用方式

不要在 main 函数中留太多逻辑,把主要逻辑包装在一个新的函数中。然后通过静态链接不同 main 函数实现构建测试程序或者部署要使用的程序。

说起来比较复杂,其实我们一直都是这么做的。

Catch2

略。Catch2 比较好的是它提供的是 CHECK 宏,能够直接展开其中的逻辑表达,符合人的习惯,不用专门区分 EQ/GE/LE 等多种情况。

GTest

gtest_main

test/CMakeLists.txt

include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG release-1.11.0
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)

add_executable(unit_tests
               calc_test.cpp
               run_test.cpp)
target_link_libraries(unit_tests PRIVATE sut gtest_main)
include(GoogleTest)
gtest_discover_tests(unit_tests)

现在链接后的 unit_tests 本身就是一个可执行的测试,可以直接运行。最后的 gtest_discover_tests(unit_tests) 是从可执行文件中找出测试项目,这样就不用手动写 add_test 了。继承 ::testing::Test 的作用之一或许就是能够在可执行文件中找到对应的类成员函数 TestBody

TEST_F 创建带有数据的一组测试:

class CalcTestSuite : public ::testing::Test {
 protected:
  Calc sut_;
};

TEST_F(CalcTestSuite, SumAddsTwoInts) {
  EXPECT_EQ(4, sut_.Sum(2, 2));
}

TEST_F(CalcTestSuite, MultiplyMultipliesTwoInts) {
  EXPECT_EQ(12, sut_.Multiply(3, 4));
}

TEST 创建不需要数据的函数,其中 RunTest 是一个新类名:

#include <gtest/gtest.h>
#include <string>
#include <iostream>
#include <sstream>
#include "calc.h"

using namespace std;
int run(); // declaration
TEST(RunTest, RunOutputsCorrectEquations) {
  string expected {"2 + 2 = 4\n3 * 3 = 9\n"};
  stringstream buffer;
  // redirect cout
  auto prevcoutbuf = cout.rdbuf(buffer.rdbuf());
  run();
  auto output = buffer.str();
  // restore original buffer
  cout.rdbuf(prevcoutbuf);
  EXPECT_EQ(expected, output);
}

其中 run 是在 src/ 中定义的:

int run() {
  Calc c;
  cout << "2 + 2 = " << c.Sum(2, 2) << endl;
  cout << "3 * 3 = " << c.Multiply(3, 3) << endl;
  return 0;
}

在编辑器看可以直到它们都展开成了一个类,花括号的方法是 TestBody 这个被用到的方法。前者通过值来测试,后者则检查标准输出流。而且每个单元测试都是直接或间接继承了 testing::Test,创建了一个类:前者是继承 test_suite_name,而 test_suite_name 继承 testing::Test,所以是间接继承;后者是直接继承。

注意,它们都没有定义 main 方法!这个方法是 GTest 通过让使用者链接 gtest_main 提供的

gmock

GTest 还提供了一个可链接的 target 是 gmock。Mock 是用来代替真实对象的替代物,要结合依赖注入在单元测试中被使用。

gmock 的思路是:用 MOCK_METHOD 来描述 mock 函数,用 EXPECT_CALL 来描述调用行为。好处是只用写一个 mock 类,就能在每个单元测试中描述方法的不同行为。还能检查方法的调用情况。

推荐 mock 命名:test/mocks/*_mock.{hpp,cpp,...}

使用 MOCK_METHOD:

#pragma once
#include "gmock/gmock.h"

class RandomNumberGeneratorMock : public RandomNumberGenerator {
public:
    MOCK_METHOD(int, Get, (), (override));
    // MOCK_METHOD(<return type>, <method name>, (<argument list>), (<keywords>))
};

使用 EXPECT_CALL

#include <gtest/gtest.h>
#include "calc.h"
#include "mocks/rng_mock.h"
using namespace ::testing;
class CalcTestSuite : public Test {
protected:
    RandomNumberGeneratorMock rng_mock_;
    Calc sut_{&rng_mock_};
};
    TEST_F(CalcTestSuite, AddRandomNumberAddsThree) {
    EXPECT_CALL(rng_mock_, Get()).Times(1).WillOnce(Return(3));
    EXPECT_EQ(4, sut_.AddRandomNumber(1));
}

Generating test coverage reports

分析工具可以用 gcov,图形化显示工具可以用 LCOV。

gcov 是 gcc 提供的,当然 clang 也能用自己的方式生成相同格式的输出。需要注意以下几点:

  1. 用 Debug 模式编译,因为优化选项会使得对代码行的追溯不准确;还要打开编译器的代码覆盖选项,对 gcc 和 clang 来说都是 --coverage。(这个选项是在 compile 和 link 阶段都要给的。)
  2. 链接 gcov 库。
  3. 运行 baseline 但不运行测试。
  4. 运行测试,收集产生的 .gcda 文件。
# sut: system under test
target_compile_options(sut PRIVATE --coverage)
target_link_options(sut PUBLIC --coverage)
#                       ^^^ 注意这个 PUBLIC

书中还有一步是清理过时的 .gcda 文件避免干扰。

find_program 来找程序,这样就能在配置阶段提前暴露问题,而不是运行起来才出现问题。

Caution

书上的操作复现不了。