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 也能用自己的方式生成相同格式的输出。需要注意以下几点:
- 用 Debug 模式编译,因为优化选项会使得对代码行的追溯不准确;还要打开编译器的代码覆盖选项,对 gcc 和 clang 来说都是
--coverage
。(这个选项是在 compile 和 link 阶段都要给的。) - 链接
gcov
库。 - 运行 baseline 但不运行测试。
- 运行测试,收集产生的
.gcda
文件。
# sut: system under test
target_compile_options(sut PRIVATE --coverage)
target_link_options(sut PUBLIC --coverage)
# ^^^ 注意这个 PUBLIC
书中还有一步是清理过时的 .gcda
文件避免干扰。
用
find_program
来找程序,这样就能在配置阶段提前暴露问题,而不是运行起来才出现问题。
Caution
书上的操作复现不了。