Warning

本文不是教程,只是一个模板,方便本人复制粘贴而已

部分内容参考自博客 GTest / GMock 单元测试实践手册,详细的可以进博客学习

项目架构

假定项目的结构为:

Terminal window
.
├── build
├── CMakeLists.txt
├── Makefile
├── README.md
├── scripts
├── src
└── main.cpp
└── tests
├── CMakeLists.txt
└── tree.cpp

我们在 tests 中写单元测试,其中,一个 cpp 文件是一个大的单元测试

CMakeLists

我们在根目录的 CMakeLists.txt 的最后写上:

...
enable_testing()
add_subdirectory(tests)

然后,在 tests/CMakeLists.txt 中写上:

# Find required packages
find_package(GTest REQUIRED)
# Find all test source files
file(GLOB TEST_SOURCES "*.cpp")
# Create Library to Test
...
# For each test file, create a separate test executable
foreach(TEST_SOURCE ${TEST_SOURCES})
get_filename_component(TEST_NAME ${TEST_SOURCE} NAME_WE)
add_executable(${TEST_NAME}_test ${TEST_SOURCE})
target_link_libraries(${TEST_NAME}_test
${CUSTOM_LIBS}
)
# Add to CTest
add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}_test)
endforeach()

然后我们在根目录的 Makefile 下创建一个 test 目标:

.PHONY: all build test
# Default target
all: build
build:
@cmake -S . -B build
@cmake --build build -- -j$(shell nproc)
# Run tests
test: build
@cd build && ctest --output-on-failure
# Run specific test by name
test-%: build
@cd build && ctest -R $* --output-on-failure
# Run tests with verbose output
test-verbose: build
@cd build && ctest -V

这样,我们写完测试后即可通过运行

Terminal window
make test

做单元测试

单元测试模板

这里我们通过 TestFixture 的方式来写单元测试,例如现在我有一个二叉树 PartitionTree,那么我们的单元测试如下:

#include "partition_tree.hpp"
#include <gtest/gtest.h>
#include <sys/types.h>
class PartitionTreeTest : public ::testing::Test {
public:
PartitionTree tree;
std::string debug_message() {
std::stringstream ss;
return tree.to_string();
}
protected:
void SetUp() override { tree.init(n_threads); }
void TearDown() override { tree.clear(); }
std::vector<int> assumptions = {1, 2, 3, 4, 5, 6, 7};
int depth = 3;
int n_threads = 8;
};
TEST_F(PartitionTreeTest, BasicInitializationBinMode) {
tree.init_with_binary(assumptions, depth, n_threads);
EXPECT_TRUE(tree.m_root != nullptr);
for (int i = 0; i < n_threads; i++) {
EXPECT_TRUE((int)tree.collect_pre_decision(i).size() == depth);
}
EXPECT_TRUE(
reinterpret_cast<Node *>(tree.m_wid_to_addr[0])->m_parent->m_split_lit ==
3);
}
TEST_F(PartitionTreeTest, BasicInitializationScaMode) {
tree.init_with_scattering(assumptions, 2);
auto w0_pd = tree.collect_pre_decision(0);
auto w1_pd = tree.collect_pre_decision(1);
EXPECT_TRUE(w0_pd.size() == 1 && w1_pd.size() == 1);
EXPECT_TRUE(w0_pd[0] == 1 && w1_pd[0] == -1) << debug_message();
}
TEST_F(PartitionTreeTest, BasicUpdateInBinMode) {
tree.init_with_binary(assumptions, depth, n_threads);
u_int64_t w1_addr = tree.m_wid_to_addr[1];
tree.update(0, 1, 9);
u_int64_t w1_addr_new = tree.m_wid_to_addr[1];
EXPECT_TRUE(w1_addr != w1_addr_new);
auto w0_pd = tree.collect_pre_decision(0);
auto w1_pd = tree.collect_pre_decision(1);
EXPECT_TRUE(w0_pd.size() == 4);
EXPECT_TRUE(w1_pd.size() == 4);
EXPECT_TRUE(w0_pd.back() == -9);
EXPECT_TRUE(w1_pd.back() == 9);
}
TEST_F(PartitionTreeTest, BasicUpdateInScaMode) {
tree.init_with_scattering(assumptions, n_threads);
u_int64_t w1_addr = tree.m_wid_to_addr[1];
tree.update(0, 1, 9);
u_int64_t w1_addr_new = tree.m_wid_to_addr[1];
EXPECT_TRUE(w1_addr != w1_addr_new) << debug_message();
auto w0_pd = tree.collect_pre_decision(0);
auto w1_pd = tree.collect_pre_decision(1);
EXPECT_TRUE(w0_pd.size() == 2);
EXPECT_TRUE(w1_pd.size() == 2);
EXPECT_TRUE(w0_pd.back() == -9);
EXPECT_TRUE(w1_pd.back() == 9);
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

这个文件是一个大测试点,其中,每个小测试点 Test Case 通过 TEST_F(TestFixure, TestCaseName) 实现,我们这么写的原因是为了可以共享一些测试变量,也可以少写一些代码(

Tip

继承自 Test 的类,其 SetUp 函数会在每个 Test Case 执行之前被调用,而 TearDown 函数则会在每个 Test Case 执行之后被调用。