CMake 系列(十六):用 Google Test 做单元测试

本篇演示如何在 CMake 的帮助下使用 Google Test 框架实现单元测试。Google Test 不仅仅是一个头文件,更是一个库,包含需要构建和链接的源文件。可以把它们与我们的代码项目放在一起,但为了使代码项目更加轻量级,我们选择在配置时下载一个版本确定的 Google Test,然后构建框架并链接它。这里使用较新的 FetchContent 模块(从 CMake 3.11 开始可用),本篇将讨论该模块在底层是如何工作的,并演示如何用 ExternalProject_Add 对其进行模拟。

注意:本示例在 CMake 3.11 或更高版本中有效,并且已经在 GNU/Linux、macOS 和 Windows 上进行过测试。代码仓库中另有一个支持 CMake 3.5 的版本。

准备工作

main.cpp、sum_integers.cpp 和 sum_integers.hpp 保持不变,只需修改 test.cpp:

#include "sum_integers.hpp"
#include "gtest/gtest.h"

#include <vector>

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

TEST(example, sum_zero) {
  auto integers = {1, -1, 2, -2, 3, -3};
  auto result = sum_integers(integers);
  ASSERT_EQ(result, 0);
}

TEST(example, sum_five) {
  auto integers = {1, 2, 3, 4, 5};
  auto result = sum_integers(integers);
  ASSERT_EQ(result, 15);
}

如上面的代码所示,我们只在项目中显式包含 gtest.h,其他 Google Test 源文件不放入代码项目仓库,而是在配置时用 FetchContent 模块下载。

具体实施

下面的步骤描述了如何设置 CMakeLists.txt,使用 GTest 编译可执行文件及其相应的测试。

与前两个示例相比,CMakeLists.txt 的开头基本没有变化,而使用 FetchContent 模块要求 CMake 3.11:

# set minimum cmake version
cmake_minimum_required(VERSION 3.11 FATAL_ERROR)

# project name and language
project(recipe-03 LANGUAGES CXX)

# require C++11
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)

# example library
add_library(sum_integers sum_integers.cpp)

# main code
add_executable(sum_up main.cpp)
target_link_libraries(sum_up sum_integers)

然后引入一个 if,检查 ENABLE_UNIT_TESTS。默认情况下它为 ON,但可以设置为 OFF,以便在没有网络连接时跳过单元测试的构建:

option(ENABLE_UNIT_TESTS "Enable unit tests" ON)
message(STATUS "Enable testing: ${ENABLE_UNIT_TESTS}")

if(ENABLE_UNIT_TESTS)
  # all the remaining CMake code will be placed here
endif()

if 内部包含 FetchContent 模块,声明要获取的内容,并查询其属性:

  include(FetchContent)

  FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG        release-1.8.0
  )

  FetchContent_GetProperties(googletest)

如果内容还没有获取到,将尝试获取并配置它,这一步会添加几个可供链接的目标。本例中我们对 gtest_main 感兴趣。示例中还包含两个变通设置,用于在 Visual Studio 下顺利编译:

  if(NOT googletest_POPULATED)
    FetchContent_Populate(googletest)

    # Prevent GoogleTest from overriding our compiler/linker options
    # when building with Visual Studio
    set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)

    # Prevent GoogleTest from using PThreads
    set(gtest_disable_pthreads ON CACHE BOOL "" FORCE)

    # adds the targets: gtest, gtest_main, gmock, gmock_main
    add_subdirectory(
      ${googletest_SOURCE_DIR}
      ${googletest_BINARY_DIR}
      )

    # Silence std::tr1 warning on MSVC
    if(MSVC)
      foreach(_tgt gtest gtest_main gmock gmock_main)
        target_compile_definitions(${_tgt}
          PRIVATE
            "_SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING"
          )
      endforeach()
    endif()
  endif()

然后,使用 target_sources 和 target_link_libraries 命令定义 cpp_test 可执行目标,并指定它的源文件:

  add_executable(cpp_test "")

  target_sources(cpp_test
    PRIVATE
      test.cpp
    )

  target_link_libraries(cpp_test
    PRIVATE
      sum_integers
      gtest_main
    )

最后,使用 enable_testing 和 add_test 命令定义单元测试:

  enable_testing()

  add_test(
    NAME google_test
    COMMAND $<TARGET_FILE:cpp_test>
    )

endif()

现在配置、构建并测试项目:

$ mkdir -p build
$ cd build
$ cmake ..
$ cmake --build .
$ ctest

输出如下:

Test project /home/user/cmake-cookbook/chapter-04/recipe-03/cxx-example/build
    Start 1: google_test
1/1 Test #1: google_test ...................... Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
Total Test time (real) = 0.00 sec

也可以直接运行 cpp_test:

$ ./cpp_test

输出如下:

[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from example
[ RUN      ] example.sum_zero
[ OK       ] example.sum_zero (0 ms)
[ RUN      ] example.sum_five
[ OK       ] example.sum_five (0 ms)
[----------] 2 tests from example (0 ms total)
[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran. (0 ms total)
[ PASSED  ] 2 tests.

工作原理

FetchContent 模块对 ExternalProject 模块的功能进行了封装,可以在配置时填充内容,它从 CMake 3.11 起成为 CMake 的标准组成部分。而 ExternalProject_Add() 则在构建时进行下载操作。FetchContent 模块让获取内容的动作提前到配置阶段完成,主项目和外部项目(本例中即 Google Test)只在第一次执行 CMake 时获取一次,并可以通过 add_subdirectory 嵌套进来。

为了获取 Google Test,首先声明外部内容:

include(FetchContent)

FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        release-1.8.0
)

本例中,我们获取了一个带有特定标签的 Git 仓库(release-1.8.0),但也可以从 Subversion、Mercurial 或 HTTP(S) 源获取一个外部项目。可用选项与 ExternalProject_Add 命令的选项一致。

调用 FetchContent_Populate() 之前,应先用 FetchContent_GetProperties() 命令检查该内容是否已经被填充过;否则,再次调用 FetchContent_Populate() 时就会抛出错误。

FetchContent_Populate(googletest) 用于填充源码,并定义 googletest_SOURCE_DIR 和 googletest_BINARY_DIR 两个变量,可以用它们把 Google Test 项目纳入构建(使用 add_subdirectory(),因为它恰好本身也是一个 CMake 项目):

add_subdirectory(
  ${googletest_SOURCE_DIR}
  ${googletest_BINARY_DIR}
  )

前面定义了以下目标:gtest、gtest_main、gmock 和 gmock_main。本配置中,作为单元测试的库依赖项,我们只对 gtest_main 目标感兴趣:

target_link_libraries(cpp_test
  PRIVATE
    sum_integers
    gtest_main
)

构建代码时,可以看到 Google Test 是如何被正确配置和构建的。有时我们希望升级到更新的 Google Test 版本,这时需要更改的唯一一行就是说明 GIT_TAG 的那一行。

更多信息

了解了 FetchContent 及其构建时阶段的近亲 ExternalProject_Add 之后,后续示例中还会重新讨论这些命令。

本示例在配置时获取源代码;也可以把 Google Test 安装到系统环境中,再使用 FindGTest 模块来检测库和头文件。从 3.9 版开始,CMake 还提供了一个 GoogleTest 模块,它提供了 gtest_add_tests 函数,通过搜索源代码中的 Google Test 宏,可以自动添加测试。

当然,Google Test 还有许多有趣的特性,这里不再一一展开。

系列导航


   转载规则


《CMake 系列(十六):用 Google Test 做单元测试》 由 吴杭沉 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
CMake 系列(十七):用函数和宏重用代码 CMake 系列(十七):用函数和宏重用代码
任何编程语言中,函数都允许我们抽象(隐藏)细节并避免代码重复,CMake 也不例外。本篇将以宏和函数为例进行讨论,并介绍一个宏,用于方便地定义测试并设置测试的顺序。我们的目标是定义一个宏,用来替换 add_test 和 set_tests_
2022-07-05
下一篇 
CMake 系列(十五):检测 Boost 库 CMake 系列(十五):检测 Boost 库
Boost 是一组 C++ 通用库。这些库提供了许多现代 C++ 项目不可或缺、但 C++ 标准尚未提供的功能,例如元编程、处理可选参数和文件系统操作等组件。其中许多特性后来被 C++11、C++14 和 C++17 标准所采纳,但对于需要
2022-06-21
  目录