0. 简介

在开发大量的程序后,我们发现每一个之前留下的BUG都需要耗费大量的时间去排查设计。这样会造成算法开发人员耗费大量的时间和精力。而gtest就是这样的一个工具,它是Google的一个开源框架,主要功能是用于编写单元测试,从而检查自己的程序是否符合预期行为。这当然是QA(测试工程师)最常用的工具,但是作为一名算法工程师也需要清楚自己的算法是否符合设计需求以及规范。

1. 认识 gtest

1.1 安装 gtest

gtest作为一种测试框架,它可以在垮平台(Linux,Mac OS X,Windows,Cygwin,Windows CE和Symbian)编写C++测试程序。它是基于xUnit架构的测试框架,支持自动发现测试,丰富的断言集,用户定义的断言,death测试,致命与非致命的失败,类型参数化测试,各类运行测试的选项和XML的测试报告。

#下载链接
# git clone https://github.com/google/googletest.git
# mkdir -p googletest/build
# cd googletest/build
# cmake ..
# make -j8
# sudo make install
# 或者直接apt安装
sudo apt-get install libgtest-dev
# 更新Apt工具软件包列表
sudo apt update
# 装GCC、MAKE、GDB等工具
sudo apt install build-essential gdb

通过上述操作,GTest库文件安装在/usr/local/lib/目录,头文件安装在/usr/local/include/目录。

1.2 gtest编程

这里为了验证gtest安装的情况,我们给出了一个demo程序示例。
CMakeLists.txt:

cmake_minimum_required(VERSION 3.1.0)
project(main VERSION 0.1.7)

# 添加库文件,库文件名称为:${PROJECT_NAME},也就是xxx
# 依赖的实现文件是xxx.cpp
#add_library(${PROJECT_NAME} xxx.cpp)

# Generate the testing executable file.
include(CTest)
# 启用测试
enable_testing()
# 添加测试可执行文件,即最后生成的可执行文件名称为:main_test,
# 依赖的实现文件是main.cpp
add_executable(${PROJECT_NAME}_test main.cpp)
# 寻找GTest和Threads库,两个库必需
find_package(GTest REQUIRED)
find_package(Threads REQUIRED)
# 包含GTest的头文件目录
include_directories(${GTEST_INCLUDE_DIRS})
# 生成测试可执行文件main_test需要链接gtest、pthread库
target_link_libraries(${PROJECT_NAME}_test ${GTEST_BOTH_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} )

# 让CMake能够发现main_test文件,以便使用ctest命令时能够自动运行测试文件
gtest_discover_tests(${PROJECT_NAME}_test)
# 当前及其下级目录打开测试功
#set(FooTestArgs --foo 1 --bar 2)
#gtest_add_tests(FooTest "${FooTestArgs}" AUTO)
# CMake 原生支持的测试很简单
# add_test(StringOperationTest ${PROJECT_NAME}_test)


set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)

main.cpp

#include <gtest/gtest.h>

int add(int a, int b) { 
    return a + b; 
}
TEST(testCase, test0) { 
    EXPECT_EQ(add(8, 11), 19); 
}

int main(int argc, char **argv) {
  testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}

1.3 gtest_add_testsgtest_discover_tests()

这里着重强调一下gtest_add_testsgtest_discover_tests(),这两个命令旨在代替使用 add_test() 注册测试,并且将为每个Google Test测试用例创建一个单独的CTest测试。

通过扫描源代码的谷歌测试宏,用CTest自动添加测试

```bash
gtest_add_tests(TARGET target
                [SOURCES src1...]
                [EXTRA_ARGS arg1...]
                [WORKING_DIRECTORY dir]
                [TEST_PREFIX prefix]
                [TEST_SUFFIX suffix]
                [SKIP_DEPENDENCY]
                [TEST_LIST outVar]
)

# test
include(GoogleTest)
add_executable(FooTest FooUnitTest.cxx)
gtest_add_tests(TARGET      FooTest
                TEST_SUFFIX .noArgs
                TEST_LIST   noArgsTests
)
set_tests_properties(${noArgsTests}   PROPERTIES TIMEOUT 10)

TARGET target
指定Google测试可执行文件,它必须是一个已知的CMake可执行文件目标。当运行测试时,CMake将替代构建的可执行文件的位置。

SOURCES src1...
提供后,将仅扫描列出的文件以查找测试用例。如果未提供此选项,则将使用指定 target 的 SOURCES 属性获取源列表。

EXTRA_ARGS arg1...
在命令行上传递给每个测试用例的任何额外参数。

WORKING_DIRECTORY dir
指定运行已发现的测试用例的目录,如果没有提供这个选项,则使用当前的二进制目录。如果没有提供这个选项,则使用当前的二进制目录。

TEST_PREFIX prefix
指定在每个发现的测试用例名称之前添加的 prefix 。当在多次调用 gtest_add_test() 的过程中使用相同的源文件,但使用不同的 EXTRA_ARGS 时,这将很有用。

TEST_SUFFIX suffix
与 TEST_PREFIX 相似,不同之处在于在每个发现的测试用例的名称后都添加了 suffix 。无论 TEST_PREFIX 和 TEST_SUFFIX 可以指定。

通常情况下,这个函数会创建一个依赖关系,如果任何被扫描的源代码被改变,这个依赖关系会导致 CMake 被重新运行。这是为了确保发现的测试列表被更新。如果不需要这种行为 (在实际编写测试用例的时候可能会这样),可以使用这个选项来防止依赖关系被添加。

```TEST_LIST outVar

由 outVar 命名的变量将在调用范围中填充发现的测试用例列表。这允许调用者执行诸如操作发现的测试的测试属性之类的操作。

CMake 自从 3.10 提供了新的方法,也就是我们下面要说的gtest_discover_tests()

gtest_discover_tests(target
                     [EXTRA_ARGS arg1...]
                     [WORKING_DIRECTORY dir]
                     [TEST_PREFIX prefix]
                     [TEST_SUFFIX suffix]
                     [NO_PRETTY_TYPES] [NO_PRETTY_VALUES]
                     [PROPERTIES name1 value1...]
                     [TEST_LIST var]
                     [DISCOVERY_TIMEOUT seconds]
                     [XML_OUTPUT_DIR dir]
                     [DISCOVERY_MODE <POST_BUILD|PRE_TEST>]
)

# test
enable_testing()
include(GoogleTest)
find_package(GTest 1.10.0)

add_executable(test test.cpp)
target_link_libraries(test GTest::gtest GTest::gtest_main GTest::gmock
                        GTest::gmock_main)
gtest_discover_tests(test)

target
指定Google测试可执行文件,它必须是一个已知的CMake可执行文件目标。当运行测试时,CMake将替代构建的可执行文件的位置。

EXTRA_ARGS arg1...
在命令行上传递给每个测试用例的任何额外参数。

WORKING_DIRECTORY dir
指定运行已发现的测试用例的目录,如果没有提供这个选项,则使用当前的二进制目录。如果没有提供这个选项,则使用当前的二进制目录。

TEST_PREFIX prefix
指定在每个发现的测试用例名称之前添加的 prefix 。当在多次调用 gtest_discover_tests() 的过程中使用相同的测试可执行文件,但使用不同的 EXTRA_ARGS 时,这将很有用。

TEST_SUFFIX suffix
与 TEST_PREFIX 相似,不同之处在于在每个发现的测试用例的名称后都添加了 suffix 。无论 TEST_PREFIX 和 TEST_SUFFIX 可以指定。

NO_PRETTY_TYPES
默认情况下,类型参数化测试的类型索引会被CTest测试名称中的实际类型名称所取代。如果这种行为是不可取的(例如,因为类型名很累赘),这个选项将抑制这种行为。

NO_PRETTY_VALUES
默认情况下,值参数化测试的值索引会被CTest测试名称中的实际值取代。如果这种行为不可取(例如,因为值字符串很累赘),这个选项将抑制这种行为。

PROPERTIES name1 value1...
指定在通过调用 gtest_discover_tests() 发现的所有测试上设置的附加属性。

TEST_LIST var
使测试列表在变量 var 中可用,而不是默认的 _TESTS 中。当在多次调用 gtest_discover_tests() 的过程中使用同一测试可执行文件时,此功能很有用。请注意,此变量仅在CTest中可用。

DISCOVERY_TIMEOUT num
3.10.3版的新内容。指定CMake等待测试枚举可用测试的时间(以秒为单位)。如果测试花费的时间超过此时间,则发现(和您的构建)将失败。大多数测试可执行文件会很快枚举其测试,但是在某些特殊情况下,测试可能需要更长的超时时间。默认值为5。另请参见 execute_process() 的 TIMEOUT 选项

1.4 gtest 断言(assertion)

在gtest中,是通过断言(assertion)来判断代码实现的功能是否符合预期。断言的结果分为successnon-fatal failturefatal failture

根据断言失败的种类,gtest提供了两种断言函数:

  • success:即断言成功,程序的行为符合预期,程序继续向下允许。

  • non-fatal failure:即断言失败,但是程序没有直接crash,而是继续向下运行。

    gtest提供了宏函数EXPECT_XXX(expected, actual):如果condition(expected, actual)返回false,则EXPECT_XXX产生的就是non-fatal failure错误,并显示相关错误。

  • fatal failure:断言失败,程序直接crash,后续的测试案例不会被运行。

    gtest提供了宏函数ASSERT_XXX(expected, actual)。在写单元测试时,更加倾向于使用EXPECT_XXX,因为ASSERT_XXX是直接crash退出的,可能会导致一些内存、文件资源没有释放,因此可能会引入一些bug。

在这里插入图片描述

2. ROS中使用gtest

在ROS中需要对CMakeLists.txt加入以下代码:

if(CATKIN_ENABLE_TESTING)
    add_rostest_gtest(test_node test/test.cpp src/foo.cpp)
    target_link_libraries(test_node ${catkin_LIBRARIES} gtest)
endif()

ROS2则是


# 如果是测试代码
if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # 加入gtest包
  find_package(ament_cmake_gtest REQUIRED)
  # the following line skips the linter which checks for copyrights
  # uncomment the line when a copyright and license is not present in all source files
  # set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # uncomment the line when this package is not in a git repo
  # set(ament_cmake_cpplint_FOUND TRUE)
  set(TEST
      test/main.cpp
      test/clientTest.cpp
    )
  # 生成加入gtest的test执行文件。${PROJECT_NAME}_test为自定义的test执行文件名称;test/demo_test.cpp为test源码路径
  # 注意这里导包的时候,不再需要将 .h 文件导入进来,因为在 client.cpp中已经导入了我们需要使用到的.h文件
  # 另外,注意这里不能导入开发代码中的 main.cpp,因为已经有了一个测试的main.cpp
  ament_add_gtest(${PROJECT_NAME}_test ${TEST} src/client.cpp)
  # 务必注意这里需要添加的依赖包
  ament_target_dependencies(${PROJECT_NAME}_test rclcpp tutorial_interfaces)

  install(TARGETS
          ${PROJECT_NAME}_test
          # 将生成的test执行文件安装到DESTINATION后的路径下
          DESTINATION lib/${PROJECT_NAME})                                       
  ament_lint_auto_find_test_dependencies()
endif()

主函数

#include <ros/ros.h>
// gtest头文件
#include <gtest/gtest.h>

// 使用 GTest 库,所以 继承了其中的基类,我们只需要在里面调用我们想测试的函数,testCase 用于区分不同的测试用例。
class Test:public testing::Test
{
protected:
    /* TestSuite级别的,在第一个TEST之前运行 */
    static void SetUpTestCase()
    {
    }
    /* TestSuite级别的,在最后一个TEST之后运行 */
    static void TearDownTestCase()
    {
    }
    /* TestCase级别的,每个TestCase前执行 */
    virtual void SetUp()
    {
    }
    /* TestCase级别的,每个TestCase后执行 */
    virtual void TearDown()
    {
    }
};

// 声明第一个测试例
// TestSuite是测试对象的名称,这个名称是任意的,但是最好具有针对性
// testCase1是测试用例的名称,这个是唯一的,最好说明测试用例的用途
TEST(TestSuite, testCase1) {
  <test things here, calling EXPECT_* and/or ASSERT_* macros as needed>
  ASSERT_EQ(a, foo::func(b, c)); // 仅做示范,作用:测试函数foo::func返回结果与a是否相等
}

// 声明第二个测试例
TEST(TestSuite, testCase2) {
  <test things here, calling EXPECT_* and/or ASSERT_* macros as needed>
}

int main(int argc, char **argv) {
  // 程序运行之后,可以在终端显示测试结果,也会以.xml文件的格式存储到指定路径文件夹,
  // 且.xml文件的名字默认是测试程序的节点的名字,多次运行测试程序,不会覆盖之前的测试文件。
  testing::GTEST_FLAG(output) = "xml:/home/jd/";
  // 初始化测试器
  testing::InitGoogleTest(&argc, argv);

  ros::init(argc, argv, "test_node");
  ros::NodeHandle nh;

  // 开始执行所有的测试,如果全部通过则返回0,否则返回1
  // 此步是必须的,否则不会执行测试
  return RUN_ALL_TESTS();
}

参考链接

https://blog.csdn.net/davidhopper/article/details/120625593
https://juejin.cn/post/6847902218335551496
https://runebook.dev/zh-CN/docs/cmake/module/googletest
https://mp.weixin.qq.com
https://leooo48.github.io/2018/08/14/gtest/
https://blog.csdn.net/m0_60028455/article/details/121285395