使用GoogleTest做单元测试(一)

初识GTest

Posted by Simon on June 7, 2020

“Better code, better life. ”

使用GoogleTest做单元测试(一)

【本文部分翻译自GTest官方文档

测试并不只是测试工程师的责任,对于开发工程师,为了保证发布给测试环节的代码具有足够好的质量( Quality ),为所编写的功能代码编写适量的单元测试是十分必要的。

单元测试( Unit Test ,模块测试)是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确,通过编写单元测试可以在编码阶段发现程序编码错误,甚至是程序设计错误。

单元测试不但可以增加开发者对于所完成代码的自信,同时,好的单元测试用例往往可以在回归测试的过程中,很好地保证之前所发生的修改没有破坏已有的程序逻辑。因此,单元测试不但不会成为开发者的负担,反而可以在保证开发质量的情况下,加速迭代开发的过程。

对于单元测试框架,目前最为大家所熟知的是 JUnit 及其针对各语言的衍生产品, C++ 语言所对应的 JUnit 系单元测试框架就是 CppUnit 。但是由于 CppUnit 的设计严格继承自 JUnit ,而没有充分考虑 C++ 与 Java 固有的差异(主要是由于 C++ 没有反射机制,而这是 JUnit 设计的基础),在 C++ 中使用 CppUnit 进行单元测试显得十分繁琐,这一定程度上制约了 CppUnit 的普及。笔者在这里要跟大家介绍的是一套由 google 发布的开源单元测试框架( Testing Framework ): googletest

编译与安装

  • Windows下
#下载源码
git clone https://github.com/google/googletest.git
cd googletest
#使用cmake生成vs项目文件
cmake .

执行完cmake以后,当前目录下会生成.sln和.cvxproj文件,用Visual Studio打开,然后生成指定版本的链接库

  • *NIX下
#下载源码
git clone https://github.com/google/googletest.git
cd googletest
#使用cmake生成vs项目文件
cmake .
make -j

这里我使用的源码的commit ID为:4fe0180cmake 3.5.1,执行完上述命令后make的时候报了很多语法错误,应该是编译时使用的C++版本问题,在CMakeLists.txt加入:

set(CMAKE_CXX_STANDARD 14)

后一切正常。然后安装

sudo make install

使用cmake进行构建

make install后静态链接库声称在/usr/local/lib目录下,我们可以使用绝对目录来连接,当然更方便的是使用cmake来构建GTest项目,只需要在你的CMakeLists.txt中加入如下内容

if (UNIX)
    find_package(Threads REQUIRED)
    find_package(GTest REQUIRED)
    if (GTest_FOUND)
        include_directories(${GTEST_INCLUDE_DIR})
    endif ()
else ()

endif ()

add_executable(${PROJECT_NAME} ${SRC_DIR})

target_link_libraries(${PROJECT_NAME} ${GTEST_LIBRARY})
target_link_libraries(${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT})

下面我们来看一个使用GTest的例子。

Demo1

目录结构

├── build.sh
├── CMakeLists.txt
├── gtest
├── include
│   └── Configure.h
├── main.cpp
└── src
    ├── Configure.cpp
    └── ConfigureTest.cpp

Configure.h


#ifndef GTEST_CONFIGURE_H

#define GTEST_CONFIGURE_H

#include <string>

#include <vector>

class Configure
{
private:
    std::vector<std::string> vItems;

public:
    int addItem(std::string str);

    std::string getItem(int index);

    int getSize();
};


#endif //GTEST_CONFIGURE_H

Configure.cpp

#include <algorithm>

#include "Configure.h"

/**
* @brief Add an item to configuration store. Duplicate item will be ignored
* @param str item to be stored
* @return the index of added configuration item
*/
int Configure::addItem(std::string str) {
    std::vector<std::string>::const_iterator vi = std::find(vItems.begin(), vItems.end(), str);
    if (vi != vItems.end())
        return vi - vItems.begin();

    vItems.push_back(str);
    return vItems.size() - 1;
}

/**
* @brief Return the configure item at specified index.
* If the index is out of range, "" will be returned
* @param index the index of item
* @return the item at specified index
*/
std::string Configure::getItem(int index) {
    if (index >= vItems.size())
        return "";
    else
        return vItems.at(index);
}

/// Retrieve the information about how many configuration items we have had
int Configure::getSize() {
    return vItems.size();
}

ConfigureTest.cpp

#include <gtest/gtest.h>

#include "Configure.h"

TEST(ConfigureTest, addItem)
{
    // do some initialization
    auto* pc = new Configure();

    // validate the pointer is not null
    ASSERT_TRUE(pc != nullptr);

    // call the method we want to test
    pc->addItem("A");
    pc->addItem("B");
    pc->addItem("A");

    // validate the result after operation
    EXPECT_EQ(pc->getSize(), 2);
    EXPECT_STREQ(pc->getItem(0).c_str(), "A");
    EXPECT_STREQ(pc->getItem(1).c_str(), "B");
    EXPECT_STREQ(pc->getItem(10).c_str(), "");

    delete pc;
}

main.cpp

#include <gtest/gtest.h>

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

    // Runs all tests using Google Test.
    return RUN_ALL_TESTS();
}

编译运行:

cmake .
make -j
./gtest

Output:

[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from ConfigureTest
[ RUN      ] ConfigureTest.addItem
[       OK ] ConfigureTest.addItem (0 ms)
[----------] 1 test from ConfigureTest (0 ms total)

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

使用TEST宏创建测试用例

Demo1ConfigureTest.cpp文件中,使用了TEST()这个宏来创建一个Test Case,关于TEST宏,官方文档上主要有以下几点说明:

  • 使用TEST来定义或者明明一个测试函数,该函数没有返回值。
  • TEST函数和任何其他C++函数相比,只多了一点:你可以使用GTest提供的断言宏来对测试结果进行判断。
  • 测试结果由断言确定;如果测试中的任何断言失败(致命或非致命),或者测试崩溃,则整个测试失败。

这里我们再举一个简单的例子进行说明,有一个函数其声明为:

int Factorial(int n);  // Returns the factorial of n

针对这个函数的一个测试用例应该是这样的:

// Tests factorial of 0.
TEST(FactorialTest, HandlesZeroInput) {
  EXPECT_EQ(Factorial(0), 1);
}

// Tests factorial of positive numbers.
TEST(FactorialTest, HandlesPositiveInput) {
  EXPECT_EQ(Factorial(1), 1);
  EXPECT_EQ(Factorial(2), 2);
  EXPECT_EQ(Factorial(3), 6);
  EXPECT_EQ(Factorial(8), 40320);
}

GTest中的断言宏

上面我们讲了,断言宏是使用在TEST测试用例中,用来判断测试执行结果的方法。

GTest的断言宏有两种:1. 形如ASSERT_* 2. 形如EXPECT_*。第一种类似assert.h头文件中的assert方法,如果表达之为false,则直接调用abort使程序退出;而第二种即使表达式为false,也不会退出程序,只会相应日志,继续执行其他测试用例。官方推荐使用第二种。

至于*部分,主要包括一下几种

类型 含义
*_TRUE(condition) condition为真
*_FALSE(condition) condition为假
*_EQ(expected, actual) expected==actual
*_NE(val1, val2) val1!=val2
*_LT(val1, val2) val1<val2
*_LE(val1, val2) val1<=val2
*_GT(val1, val2) val1>val2
*_GE(val1, val2) val1>=val2
*_STREQ(expected_str, actual_str) 两个 C 字符串有相同的内容
*_STRNE(str1, str2) 两个 C 字符串有不同的内容
*_STRCASEEQ(expected_str, actual_str) 两个 C 字符串有相同的内容,忽略大小写
*_STRCASENE(str1, str2) 两个 C 字符串有不同的内容,忽略大小写

经排列组合以后,GTest一共有24种断言宏。

在不同的测试用例中共享数据

如果你需要在不同的测试用例中使用类似的数据,GTest提供了一种叫做test fixture的特性来满足你这种需求。

创建一个fixture

  • 创建一个继承自::testing::Test的类,其所有成员均为protected类型,因为他的成员函数/变量会在子类中被访问。
  • 把你想要在多个测试用例中共享的数据在构造函数或者SetUp()方法中初始化。
  • 如你在SetUp()中使用了动态内存,那么必须在声明一个TearDown()方法里回收这些内存。
  • 使用TEST_F(TestFixtureName, TestName)而不是TEST

【注意】

  • TEST_F()的第一个参数应该是你声明的fixture类的类名。
  • 由于C++中宏定义不允许使用单一的宏来处理所有的测试类型,所以你必须在TEST_F()之前定义一个fixture类,不然会报编译错误virtual outside class declaration.
  • 同一个测试用例里的不同的测试具有独立的fxiture对象,GTest会在下一次测试开始前删除之前的fixture对象,并创建一个新的。所以,当前测试对fxiture的修改,不会影响下一次测试。

下面是一个FIFO Queue的例子:

template <typename E>  // E is the element type.
class Queue {
 public:
  Queue();
  void Enqueue(const E& element);
  E* Dequeue();  // Returns NULL if the queue is empty.
  size_t size() const;
  ...
};

//First, define a fixture class. By convention, you should give it the name FooTest where Foo is the class being tested.

class QueueTest : public ::testing::Test {
 protected:
  void SetUp() override {
     q1_.Enqueue(1);
     q2_.Enqueue(2);
     q2_.Enqueue(3);
  }
  //In this case, TearDown() is not needed since we don't have to clean up after each test, other than what's already done by the destructor.
  // void TearDown() override {}

  Queue<int> q0_;
  Queue<int> q1_;
  Queue<int> q2_;
};

声明完fixture后,我们可以在接下来的测试中使用它

//第一个参数要是上面声明的类名
TEST_F(QueueTest, IsEmptyInitially) {
  EXPECT_EQ(q0_.size(), 0);
}

TEST_F(QueueTest, DequeueWorks) {
  int* n = q0_.Dequeue();
  EXPECT_EQ(n, nullptr);

  n = q1_.Dequeue();
  ASSERT_NE(n, nullptr);
  EXPECT_EQ(*n, 1);
  EXPECT_EQ(q1_.size(), 0);
  //需要主动delete相关对象以回收资源
  delete n;

  n = q2_.Dequeue();
  ASSERT_NE(n, nullptr);
  EXPECT_EQ(*n, 2);
  EXPECT_EQ(q2_.size(), 1);
  delete n;
}

在上面的调用过程中,GTest主要做了下面几件事:

  • 构造了一个QueueTest的实例t1
  • 调用t1.SetUp()来初始化。
  • 执行第一个测试用例IsEmptyInitially
  • 调用t1.TearDown()删除实例。
  • 重复以上过程,执行第二个测试用例DequeueWorks

测试用例的执行

Demo1中,我们通过main.cpp中的RUN_ALL_TESTS()宏来执行我们定义好的测试用例,但实际上,你可以连接我们第一步编译出的gtest_main,而不用为每一个项目写一个main方法。