使用 GoogleTest 和 CMake 进行单元测试

本文翻译自作者David Corvoysier的一篇博客:Unit testing with GoogleTest and CMake

持续集成需要强大的测试环境,以便能够尽早检测到回归问题。

典型的测试环境通常由整个系统的集成测试和每个组件的单元测试组成。

本文讲解了如何使用 GoogleTest 和 CMake 为 C++ 组件创建单元测试。

项目结构

我假设项目结构遵循上一篇文章中描述的模型:

+-- CMakeLists.txt
+-- main
|    +-- CMakeLists
|    +-- main.cpp
|
+-- test
|    +-- CMakeLists.txt
|    +-- testfoo
|       +-- CMakeLists.txt
|       +-- main.cpp
|       +-- testfoo.h
|       +-- testfoo.cpp
|       +-- mockbar.h
|
+-- libfoo
|    +-- CMakeLists.txt
|    +-- foo.h
|    +-- foo.cpp
|
+-- libbar
     +-- CMakeLists.txt
     +-- bar.h
     +-- bar.cpp

main子目录包含项目的主要目标,这是一个可执行文件,它使用强大的 libbar 后端提供超级实用的 libfoo 服务(例如,libfoo 可以是一个通用的人脸识别库,而 libbar 可以是一个基于 GPU 的图像处理库)。

test目录包含一个可执行文件,允许使用 libbar 的模拟版本测试 libfoo 服务。
来自维基百科:在面向对象编程中,模拟对象是以受控方式模仿真实对象行为的模拟对象。
对于那些感兴趣的人,这个示例项目的代码在 github 上。
如下图所示:
gtest-cmake-example

仔细看看测试目录

在我这个简单的示例中,只有一个待test的子目录,但在典型的项目中,它会包含多个子目录,每个测试程序对应一个。

测试程序基于 Google 的 Googletest 框架及其 GoogleMock 扩展。

由于所有测试程序都将使用这些软件包,因此根 CMakeLists.txt 文件应包含解析相应依赖项所需的所有指令。这时事情会变得有点棘手,因为 Google 不建议以二进制形式安装这些软件包,而是建议将它们与您的项目一起重新编译。

解决 GoogleTest 和 GoogleMock 依赖关系

至少有三个选择可以将您的项目与 GoogleTest 和 GoogleMock 集成。

方式1:将两个包集成到您的构建系统中

显然,只有当您确实拥有构建系统时,这才是一个选择,但如果确实如此,我建议您这样做。

根据您的构建系统的结构,您的实际效果可能会有所不同,但最终您应该能够使用 CMake 函数(例如内置的 find_package 或基于 pkg-config 的 pkg_check_modules)将 GoogleTest 和 GoogleMock 声明为依赖项。

find_package(PkgConfig)
pkg_check_modules(GTEST REQUIRED gtest>=1.7.0)
pkg_check_modules(GMOCK REQUIRED gmock>=1.7.0)

include_directories(
    ${GTEST_INCLUDE_DIRS}
    ${GMOCK_INCLUDE_DIRS}
)
方式2:将两个包源添加到您的项目中

将 GoogleTest 和 GoogleMock 源文件添加为 test 的子目录,可以让你将它们编译为项目的一部分。

然而,这样做确实很丑陋,我不建议你这样做……

方式3:将两个包添加为外部 CMake 项目

根据 StackOverflow 上发布的各种答案,这似乎是针对每个项目解析 GoogleTest 和 GoogleMock 依赖项的推荐方法。

它利用 CMake ExternalProject 模块从互联网上获取 GoogleTest 和 GoogleMock 源代码,并将它们编译为项目中的第三方依赖项。

下面是一个可运行的示例,其中包含一些注释,解释了具体操作:

# We need thread support
find_package(Threads REQUIRED)

# Enable ExternalProject CMake module
include(ExternalProject)

# Download and install GoogleTest
ExternalProject_Add(
    gtest
    URL https://github.com/google/googletest/archive/master.zip
    PREFIX ${CMAKE_CURRENT_BINARY_DIR}/gtest
    # Disable install step
    INSTALL_COMMAND ""
)

# Get GTest source and binary directories from CMake project
ExternalProject_Get_Property(gtest source_dir binary_dir)

# Create a libgtest target to be used as a dependency by test programs
add_library(libgtest IMPORTED STATIC GLOBAL)
add_dependencies(libgtest gtest)

# Set libgtest properties
set_target_properties(libgtest PROPERTIES
    "IMPORTED_LOCATION" "${binary_dir}/googlemock/gtest/libgtest.a"
    "IMPORTED_LINK_INTERFACE_LIBRARIES" "${CMAKE_THREAD_LIBS_INIT}"
)

# Create a libgmock target to be used as a dependency by test programs
add_library(libgmock IMPORTED STATIC GLOBAL)
add_dependencies(libgmock gtest)

# Set libgmock properties
set_target_properties(libgmock PROPERTIES
    "IMPORTED_LOCATION" "${binary_dir}/googlemock/libgmock.a"
    "IMPORTED_LINK_INTERFACE_LIBRARIES" "${CMAKE_THREAD_LIBS_INIT}"
)

# I couldn't make it work with INTERFACE_INCLUDE_DIRECTORIES
include_directories("${source_dir}/googletest/include"
                    "${source_dir}/googlemock/include")

注意:理论上可以使用 INTERFACE_INCLUDE_DIRECTORIES 变量将 GoogleTest 和 GoogleMock 的包含目录设置为目标属性,但由于这些目录在声明时尚不存在,因此无法成功。为此,我不得不显式使用 include_directories 来指定它们。

为 libfoo 编写一个 testfoo 测试程序

testfoo 程序依赖于 libfoo、GoogleTest 和 GoogleMock。

testfoo CMakeLists.txt 文件如下所示:

file(GLOB SRCS *.cpp)

add_executable(testfoo ${SRCS})

target_link_libraries(testfoo
    libfoo
    libgtest
    libgmock
)

install(TARGETS testfoo DESTINATION bin)

构建所需的库列在 target_link_libraries 下。CMake 随后会添加相应的头文件包含目录和链接选项。

testfoo 程序将为下面定义的 libfoo 库的 Foo 类提供单元测试。

foo.h
class Bar;

class Foo
{
    Foo(const Bar& bar);
    bool baz(bool useQux);
protected:
    const Bar& m_bar;
}
foo.cpp
#include "bar.h"
#include "foo.h"

Foo::Foo(const bar& bar)
 : m_bar(bar) {};

bool Foo::baz(bool useQux) {
    if (useQux) {
        return m_bar.qux();
    } else {
        return m_bar.norf();
    }
}

GoogleTest 文档中描述的示例测试程序适合单个文件,但我更喜欢将单元测试代码分成三种类型的文件。

main.cpp

main.cpp 文件只包含测试程序的main函数。您将在此处放置通用的 Googletest 宏调用,以启动测试,以及一些需要在主函数中执行的初始化操作(在本例中无需执行任何初始化操作)。

#include "gtest/gtest.h"

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

testfoo.h

该文件包含 FooTest 类的声明,它是 Foo 类的测试夹具。

#include "gtest/gtest.h"
#include "mockbar.h"

// The fixture for testing class Foo.
class FooTest : public ::testing::Test {

protected:

    // You can do set-up work for each test here.
    FooTest();

    // You can do clean-up work that doesn't throw exceptions here.
    virtual ~FooTest();

    // If the constructor and destructor are not enough for setting up
    // and cleaning up each test, you can define the following methods:

    // Code here will be called immediately after the constructor (right
    // before each test).
    virtual void SetUp();

    // Code here will be called immediately after each test (right
    // before the destructor).
    virtual void TearDown();

    // The mock bar library shaed by all tests
    MockBar m_bar;
};

mockbar.h

假设 libbar 库实现了公共的 Bar 接口,我们使用 GoogleMock 提供一个仅用于测试目的的虚假实现:

#include "bar.h"

class MockBar: public Bar
{
    MOCK_METHOD0(qux, bool());
    MOCK_METHOD0(norf, bool());
} 

这将允许我们在调用 Bar 类方法时将受控值注入 libfoo 库。
有关 GoogleMock 功能的详细描述,请参阅 GoogleMock 文档。

testfoo.cpp

该文件包含 TestFoo 夹具类的实现。

实际的测试用例就在这里编写。

我们将测试 Foo::baz() 方法的输出,首先,为模拟函数 Bar::qux() 和 Bar::norf() 方法设置默认值,然后用测试用例指定的值覆盖 Bar::norf() 的返回值。

在所有测试用例中,我们都使用 GoogleTest 期望值来验证 Foo::baz 方法的输出。

#include "mockbar.h"
#include "testfoo.h"

using ::testing::Return;

FooTest()
{
    // Have qux return true by default
    ON_CALL(m_bar,qux()).WillByDefault(Return(true));
    // Have norf return false by default
    ON_CALL(m_bar,norf()).WillByDefault(Return(false));
}

TEST_F(FooTest, ByDefaultBazTrueIsTrue) {
    Foo foo(m_bar);
    EXPECT_EQ(foo.baz(true), true);
}

TEST_F(FooTest, ByDefaultBazFalseIsFalse) {
    Foo foo(m_bar);
    EXPECT_EQ(foo.baz(false), false);
}

TEST_F(FooTest, SometimesBazFalseIsTrue) {
    Foo foo(m_bar);
    // Have norf return true for once
    EXPECT_CALL(m_bar,norf()).WillOnce(Return(true));
    EXPECT_EQ(foo.baz(false), false);
}

请参阅 GoogleTest 文档,了解有关如何使用 Gtest 创建单元测试的更详细的介绍。

构建测试

与往常一样,建议您在树外构建程序,即在与源分离的目录中构建程序。

mkdir build
cd build

首先,您需要调用 cmake 命令来生成构建文件。

cmake ..

这应该产生类似于此的输出:

-- The C compiler identification is GNU 4.8.2
-- The CXX compiler identification is GNU 4.8.2
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Looking for include file pthread.h
-- Looking for include file pthread.h - found
-- Looking for pthread_create
-- Looking for pthread_create - not found
-- Looking for pthread_create in pthreads
-- Looking for pthread_create in pthreads - not found
-- Looking for pthread_create in pthread
-- Looking for pthread_create in pthread - found
-- Found Threads: TRUE  
-- Configuring done
-- Generating done
-- Build files have been written to: ~/gtest-cmake-example/build

然后,构建项目目标。

make

以下输出对应于 GoogleTest 和 GoogleMock 自动从其存储库中获取并作为第三方依赖项构建的情况。

Scanning dependencies of target libfoo
[  7%] Building CXX object libfoo/CMakeFiles/libfoo.dir/foo.cpp.o
Linking CXX static library liblibfoo.a
[  7%] Built target libfoo
Scanning dependencies of target libbar
[ 15%] Building CXX object libbar/CMakeFiles/libbar.dir/bar.cpp.o
Linking CXX static library liblibbar.a
[ 15%] Built target libbar
Scanning dependencies of target myApp
[ 23%] Building CXX object main/CMakeFiles/myApp.dir/main.cpp.o
Linking CXX executable myApp
[ 23%] Built target myApp
Scanning dependencies of target gtest
[ 30%] Creating directories for 'gtest'
[ 38%] Performing download step (download, verify and extract) for 'gtest'
-- downloading...
     src='https://github.com/google/googletest/archive/master.zip'
     dst='/home/david/perso/gtest-cmake-example/build/test/gtest/src/master.zip'
     timeout='none'
-- [download 0% complete]
-- [download 1% complete]
...
-- [download 99% complete]
-- [download 100% complete]
-- downloading... done
-- verifying file...
     file='/home/david/perso/gtest-cmake-example/build/test/gtest/src/master.zip'
-- verifying file... warning: did not verify file - no URL_HASH specified?
-- extracting...
     src='/home/david/perso/gtest-cmake-example/build/test/gtest/src/master.zip'
     dst='/home/david/perso/gtest-cmake-example/build/test/gtest/src/gtest'
-- extracting... [tar xfz]
-- extracting... [analysis]
-- extracting... [rename]
-- extracting... [clean up]
-- extracting... done
[ 46%] No patch step for 'gtest'
[ 53%] No update step for 'gtest'
[ 61%] Performing configure step for 'gtest'
-- The C compiler identification is GNU 4.8.4
-- The CXX compiler identification is GNU 4.8.4
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Found PythonInterp: /usr/bin/python (found version "2.7.6")
-- Looking for include file pthread.h
-- Looking for include file pthread.h - found
-- Looking for pthread_create
-- Looking for pthread_create - not found
-- Looking for pthread_create in pthreads
-- Looking for pthread_create in pthreads - not found
-- Looking for pthread_create in pthread
-- Looking for pthread_create in pthread - found
-- Found Threads: TRUE
-- Configuring done
-- Generating done
-- Build files have been written to: /home/david/perso/gtest-cmake-example/build/test/gtest/src/gtest-build
[ 69%] Performing build step for 'gtest'
Scanning dependencies of target gmock
[ 14%] Building CXX object googlemock/CMakeFiles/gmock.dir/__/googletest/src/gtest-all.cc.o
[ 28%] Building CXX object googlemock/CMakeFiles/gmock.dir/src/gmock-all.cc.o
Linking CXX static library libgmock.a
[ 28%] Built target gmock
Scanning dependencies of target gmock_main
[ 42%] Building CXX object googlemock/CMakeFiles/gmock_main.dir/__/googletest/src/gtest-all.cc.o
[ 57%] Building CXX object googlemock/CMakeFiles/gmock_main.dir/src/gmock-all.cc.o
[ 71%] Building CXX object googlemock/CMakeFiles/gmock_main.dir/src/gmock_main.cc.o
Linking CXX static library libgmock_main.a
[ 71%] Built target gmock_main
Scanning dependencies of target gtest
[ 85%] Building CXX object googlemock/gtest/CMakeFiles/gtest.dir/src/gtest-all.cc.o
Linking CXX static library libgtest.a
[ 85%] Built target gtest
Scanning dependencies of target gtest_main
[100%] Building CXX object googlemock/gtest/CMakeFiles/gtest_main.dir/src/gtest_main.cc.o
Linking CXX static library libgtest_main.a
[100%] Built target gtest_main
[ 76%] No install step for 'gtest'
[ 84%] Completed 'gtest'
[ 84%] Built target gtest
Scanning dependencies of target testfoo
[ 92%] Building CXX object test/testfoo/CMakeFiles/testfoo.dir/main.cpp.o
[100%] Building CXX object test/testfoo/CMakeFiles/testfoo.dir/testfoo.cpp.o
Linking CXX executable testfoo
[100%] Built target testfoo

运行测试

一旦测试程序构建完成,您就可以单独运行它们……

test/testfoo/testfoo

…产生详细的输出…

[==========] Running 3 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 3 tests from FooTest
[ RUN      ] FooTest.ByDefaultBazTrueIsTrue

GMOCK WARNING:
Uninteresting mock function call - taking default action specified at:
~/gtest-cmake-example/test/testfoo/testfoo.cpp:8:
    Function call: qux()
          Returns: true
Stack trace:
[       OK ] FooTest.ByDefaultBazTrueIsTrue (0 ms)
[ RUN      ] FooTest.ByDefaultBazFalseIsFalse

GMOCK WARNING:
Uninteresting mock function call - taking default action specified at:
~/gtest-cmake-example/test/testfoo/testfoo.cpp:10:
    Function call: norf()
          Returns: false
Stack trace:
[       OK ] FooTest.ByDefaultBazFalseIsFalse (0 ms)
[ RUN      ] FooTest.SometimesBazFalseIsTrue
[       OK ] FooTest.SometimesBazFalseIsTrue (0 ms)
[----------] 3 tests from FooTest (0 ms total)

[----------] Global test environment tear-down
[==========] 3 tests from 1 test case ran. (0 ms total)
[  PASSED  ] 3 tests.

注意:您可以使用一个nice mock来摆脱 GoogleMock 警告。
…或者通过 CTest 进行全局…

make test

…仅生成测试摘要。

Running tests...
Test project ~/gtest-cmake-example/build
    Start 1: testfoo
1/1 Test #1: testfoo ..........................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.00 sec

参考资料

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐