G test

单元测试其一:gt

在公司需要进行单元测试的任务(单元测试可以帮助了解你团队的项目),它包括以下几个部分:

  • gtest单元测试
  • gmock集成测试
  • lcov代码覆盖率

因为要学的内容比较多,我将记录一些关键步骤,好让大家快速上手。

Gtest

googletest 是测试技术团队根据 Google 的特定要求和约束条件开发的测试框架。 无论您是在 Linux、Windows 还是 Mac 上工作,如果您编写 C++ 代码,googletest 都可以为您提供帮助。 它支持任何类型的测试,而不仅仅是单元测试。

https://google.github.io/googletest​google.github.io/googletest

GoogleTest User’s Guide

GoogleTest User’s Guide​google.github.io/googletest

玩一玩

下面是安装步骤,在此之前,请确保正确安装了cmake(>=3.4)gcc(>=5.1)

$ git clone https://github.com/google/googletest.git
$ cd googletest
$ mkdir build
$ cd build
$ cmake ..
$ make
$ sudo make install

我们在项目根目录下编写`CMakeList.txt:

cmake_minimum_required(VERSION 3.4) 
project(my_project) #设置项目名字

# GoogleTest requires at least C++11
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -std=c++11 -Wall") #设置常用的flag 这里-std=c++11采用C++11标准,
#-g支持gdb,-Wall打印所有信息

find_package(GTest REQUIRED)#查找gtest包

include_directories(${GTEST_INCLUDE_DIRS}) #包括gtest头文件


add_executable(
  mytest #生成二进制文件名
  test.cc #源文件
)

target_link_libraries(mytest ${GTEST_BOTH_LIBRARIES})#添加链接库
enable_testing() #使能测试

add_test(
  Test  #测试名
  mytest
)#关键!添加测试

之后我们编写一个简单的测试文件:

//test.cc
#include <gtest/gtest.h>
#include <numeric>
#include <vector>

TEST(MyTest, Sum){
    std::vector<int> vec{1,2,3};
    int sum = std::accumulate(vec.begin(), vec.end(),0);
    EXPECT_EQ(sum, 6);
}

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

开始编译和测试

mkdir build
cd build
cmake ..
make 
make test

然后就可以看到测试结果

#make test
Running tests...
Test project /home/duqian.du/gtest/build
    Start 1: Test1
1/1 Test #1: Test1 ............................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.00 sec

因为cmake没有make clean类似的功能,强烈建议在根目录下新建`build目录,在需要clear的时候直接删除该目录即可。


断言

googletest 断言是类似于函数调用的宏。当断言失败时,googletest 会打印断言的源文件和行号位置以及失败消息。可以提供自定义失败消息,该消息将附加到 googletest 的消息中。

有两种不同类型的断言:

  • ASSERT_* 版本在失败时会产生fatal error,并中止当前功能。
  • EXPECT_* 版本生成non-fatal error,不会中止当前功能。

注意:由于失败的 ASSERT_* 立即从当前函数返回,可能会跳过后面的清理代码,因此可能会导致空间泄漏。根据泄漏的性质,它可能值得修复,也可能不值得修复 - 因此,如果除了断言错误之外还出现堆检查器错误,请记住这一点。

要提供自定义失败消息,只需使用 << 运算符或一系列此类运算符将其流式传输到宏中。请参阅以下示例,

ASSERT_EQ(x.size(), y.size()) << "Vectors x and y are of unequal length";

for (int i = 0; i < x.size(); ++i) {
  EXPECT_EQ(x[i], y[i]) << "Vectors x and y differ at index " << i;
}

任何可以流式传输到 ostream 的内容都可以流式传输到断言宏,尤其是 C 字符串和字符串对象。如果一个宽字符串(wchar_t*、Windows 上 UNICODE 模式中的 TCHAR* 或 std::wstring)被流式传输到断言,则在打印时它将被转换为 UTF-8。

GoogleTest 提供了一组断言,用于以各种方式验证代码的行为:布尔条件、基于关系运算符比较值、验证字符串值、浮点值等等。完整断言请参考Assertions Reference

bool值检查

Fatal assertionNonfatal assertionVerifies
ASSERT_TRUE(condition);EXPECT_TRUE(condition);condition is true
ASSERT_FALSE(condition);EXPECT_FALSE(condition);condition is false

数值比较

Fatal assertionNonfatal assertionVerifies
ASSERT_EQ(expected, actual);EXPECT_EQ(expected, actual);expected == actual
ASSERT_NE(val1, val2);EXPECT_NE(val1, val2);val1 != val2
ASSERT_LT(val1, val2);EXPECT_LT(val1, val2);val1 < val2
ASSERT_LE(val1, val2);EXPECT_LE(val1, val2);val1 <= val2
ASSERT_GT(val1, val2);EXPECT_GT(val1, val2);val1 > val2
ASSERT_GE(val1, val2);EXPECT_GE(val1, val2);val1 >= val2

字符串比较

Fatal assertionNonfatal assertionVerifies
ASSERT_STREQ(expected_str, actual_str);EXPECT_STREQ(expected_str, actual_str);the two C strings have the same content
ASSERT_STRNE(str1, str2);EXPECT_STRNE(str1, str2);the two C strings have different content
ASSERT_STRCASEEQ(expected_str, actual_str);EXPECT_STRCASEEQ(expected_str, actual_str);the two C strings have the same content, ignoring case
ASSERT_STRCASENE(str1, str2);EXPECT_STRCASENE(str1, str2);the two C strings have different content, ignoring case

浮点型检查

Fatal assertionNonfatal assertionVerifies
ASSERT_FLOAT_EQ(expected, actual);EXPECT_FLOAT_EQ(expected, actual);the two float values are almost equal
ASSERT_DOUBLE_EQ(expected, actual);EXPECT_DOUBLE_EQ(expected, actual);the two double values are almost equal

对相近的两个数比较:

Fatal assertionNonfatal assertionVerifies
ASSERT_NEAR(val1, val2, abs_error);EXPECT_NEAR(val1, val2, abs_error);the difference between val1 and val2 doesn't exceed the given absolute error

测试宏

最常用的测试宏是TEST

TEST(TestSuiteName, TestName) {
  ... statements ...
}

`TestSuiteName和`TestName都必须是采用驼峰命名法的合法的C++字符,不能包含下划线!

此外还有TEST_F

TEST_F(TestFixtureName, TestName) {
  ... statements ...
}

以及TEST_P

TEST_P(TestFixtureName, TestName) {
  ... statements ...
}

那么三者的区别是什么?

  • 为静态或全局函数简单类编写单元测试时,TEST() 很有用。
  • 需要访问单元测试中的对象子例程(subroutines)时,TEST_F() 很有用。
  • 当您想使用参数编写测试时,TEST_P() 很有用。

我们分别给一个例子来方便读者理解。

- TEST

适用于简单测试场景

//简单类测试
class Base {
 public:
  // Copy constructor and assignment operator do exactly what we need, so we
  // use them.
  Base() : member_(0) {}
  explicit Base(int n) : member_(n) {}
  virtual ~Base() {}
  int member() { return member_; }

 private:
  int member_;
};

class Derived : public Base {
 public:
  explicit Derived(int n) : Base(n) {}
};

TEST(ImplicitCastTest, ConvertsPointers) {
  Derived derived(0);
  EXPECT_TRUE(&derived == ::testing::internal::ImplicitCast_<Base*>(&derived));
}

TEST(ImplicitCastTest, CanUseInheritance) {
  Derived derived(1);
  Base base = ::testing::internal::ImplicitCast_<Base>(derived);
  EXPECT_EQ(derived.member(), base.member());
}

该例子用于验证指针的隐式向上转换,以及是否能使用继承。例子来源

- TEST_F

适用于测试函数之间需要共享数据的场景,如果我们要在不同测试用例共享相同的数据,不能每个函数都初始化数据吧。我们需要采用类似于全局数据的的思想。所有测试的基类Test类提供了`SetUp()方法,我们只需要重写这个函数,并且初始化需要共享的数据结构,就能实现所有测试样例共享同一份数据。

class VectorTest : public testing::Test
{
protected:
    virtual void SetUp() override
    {
        vec.push_back(1);
        vec.push_back(2);
        vec.push_back(3);
    }
    std::vector<int> vec;
};
// 注意这里使用 TEST_F,而不是 TEST
TEST_F(VectorTest, PushBack)
{
    // 虽然这里修改了 vec,但对其它测试函数来说是不可见的
    vec.push_back(4);
    EXPECT_EQ(vec.size(), 4);
    EXPECT_EQ(vec.back(), 4);
}
TEST_F(VectorTest, Size)
{
    EXPECT_EQ(vec.size(), 3);
}
int main(int argc, char *argv[])
{
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

如果有必要,我们可以用TearDown函数来释放资源和析构:

class SomeTest : public ::testing::Test 
{
protected:
    void SetUp() override 
    {
        // ...
    }
	
    void TearDown() override 
    {	
        // ...	
    }	
};
TEST_F(SomeTest, xxxx){
}

注意SetUp和TearDown是针对每个例子的,每个例子都会执行这两个函数。如果想要针对所有例子只执行一次,可以用SetUpTestCase和TearDownTestCase:

class SomeTest : public ::testing::Test 
{
protected:
    static void SetUpTestCase() 
    {
        A *a = new A();
        // ...
    }
	
    static void TearDownTestCase()
    {	
        delete a;
        a = nullptr;
        // ...	
    }	
    A *a;
};

SomeTest::A *a = nullptr;
TEST_F(SomeTest, xxxx){
}

- TEST_P

TEST_P是实现参数化的必经之路。比如下面代码

TEST(IsPrimeTest, HandleTrueReturn)  
{  
    EXPECT_TRUE(IsPrime(3));  
    EXPECT_TRUE(IsPrime(5));  
    EXPECT_TRUE(IsPrime(11));  
    EXPECT_TRUE(IsPrime(23));  
    EXPECT_TRUE(IsPrime(17));  
}  

如果要测试1000个数,是不是要写1000行,那肯定不行。即使你愿意,但你的手可能不愿意。所以我们需要进行参数化,即指定一个参数列表就。步骤如下:

  1. 告诉gtest你的参数类型是什么,添加一个类,继承`testing::TestWithParam<T>,其中T就是你需要参数化的参数类型,比如上面的找素数例子,需要参数化一个int型的参数。
class IsPrimeParamTest : public::testing::TestWithParam<int>
{

};

2. 告诉gtest你拿到参数的值后,具体做些什么样的测试。这时候就要用到TEST_P

TEST_P(IsPrimeParamTest, HandleTrueReturn)
{
    int n =  GetParam();
    EXPECT_TRUE(IsPrime(n));
}

3. 告诉gtest你想要的测试参数范围是什么,需要使用INSTANTIATE_TEST_CASE_P这个宏。

INSTANTIATE_TEST_CASE_P(IsPrimeTest, IsPrimeParamTest, testing::Values(3, 5, 11, 23, 17));  

第一个参数是测试实例名,第二个是测试类名(和测试实例名不同!!!),第三个参数是参数生成器。注意,最后有一个分号。这里类名需要和TEST_P第一个参数相同,也就是IsPrimeParamTest。

INSTANTIATE_TEST_SUITE_P(InstantiationName,TestSuiteName,param_generator)

然后我们也可以用结构体作为参数:

 using Params = std::tuple<int64_t,int64_t,int64_t>;
    
class BitMapAllocTest: public ::testing::TestWithParam<Params>{
    protected:
   void SetUp() override{
            int64_t block_size = std::get<0> (GetParam());
            int64_t device_size = std::get<2> (GetParam()); 
            alloc = new BitMapAllocator(g_myfs_context, device_size, block_size);
        }

        void TearDown() override{
          alloc->shutdown();
          delete alloc;          
        }
        BitMapAllocator * alloc;
      
    };

  TEST_P(BitMapAllocTest, test_bitmap_alloc_init)
  {
          ...
  }
INSTANTIATE_TEST_CASE_P(BitMapAllocTest, BitMapAllocTest, ::testing::Values(
      Params(1024,64,1024*1024*4),
      Params(1024,32,1024*1024*4),
)
);

这里巧妙运用了std::tuple元组来生成一些列参数列表,使用std::get<x>([tuple obj]) 来获取第X个参数,我们初始化一个全局的alloc,这样就不需要在每个TEST_P都新建对象了。

Google提供了一系列参数生成器(第三个参数)

函数含义
Range(begin, end[, step])范围在begin~end之间,步长为step,不包括end
Values(v1, v2, ..., vN)v1,v2到vN的值
ValuesIn(container) and ValuesIn(begin, end)从一个C类型的数组或是STL容器,或是迭代器中取值
Bool()取false 和 true 两个值
Combine(g1, g2, ..., gN)将g1,g2,...gN进行排列组合,g1,g2,...gN本身是一个参数生成器,每次分别从g1,g2,..gN中各取出一个值,组合成一个元组(Tuple)作为一个参数。

测试的详细结果在`build/Testing/Temporary/LastTest.log 中。

TEST_P极大地方便了程序员的测试!



- TYPED_TEST

想一想如果我们有各种类型参数需要测试,能不能像泛型一样来测试各种各样的参数呢。

答案是Yes

  1. 定义一个模板类,继承`testing::Test,并使用泛型来定义参数类型
template <typename T>
class FooTest : public testing::Test {
 public:
  
  typedef std::list<T> List;
  static T shared_;
  T value_;
};

2. 接着我们定义需要测试到的具体数据类型,比如下面定义了需要测试char,int和unsigned int :

typedef testing::Types<char, int, unsigned int> MyTypes;
TYPED_TEST(FooTests, MyTest);

3. 又是一个新的宏,来完成我们的测试案例,在声明模版的数据类型时,使用TypeParam

TYPED_TEST(FooTest, DoesBlah) {
  // Inside a test, refer to the special name TypeParam to get the type
  // parameter.  Since we are inside a derived class template, C++ requires
  // us to visit the members of FooTest via 'this'.
  TypeParam n = this->value_;

  // To visit static members of the fixture, add the 'TestFixture::'
  // prefix.
  n += TestFixture::shared_;

  // To refer to typedefs in the fixture, add the 'typename TestFixture::'
  // prefix.  The 'typename' is required to satisfy the compiler.
  typename TestFixture::List values;
  values.push_back(n);
  
}

上面的例子看上去也像是类型的参数化,但是还不够灵活,因为需要事先知道类型的列表。gtest还提供一种更加灵活的类型参数化的方式,允许你在完成测试的逻辑代码之后再去考虑需要参数化的类型列表,并且还可以重复的使用这个类型列表。下面也是官方的例子:

template <typename T>
class FooTest : public testing::Test {
  
};

TYPED_TEST_CASE_P(FooTest);

4. 又是一个新的宏TYPED_TEST_P类完成我们的测试案例:

复制代码TYPED_TEST_P(FooTest, DoesBlah) {
  // Inside a test, refer to TypeParam to get the type parameter.
  TypeParam n = 0;
  
}

TYPED_TEST_P(FooTest, HasPropertyA) {  }

5. 接着,我们需要我们上面的案例,使用REGISTER_TYPED_TEST_CASE_P宏,第一个参数是testcase的名称,后面的参数是test的名称

REGISTER_TYPED_TEST_CASE_P(FooTest, DoesBlah, HasPropertyA);

接着指定需要的类型列表:

typedef testing::Types<char, int, unsigned int> MyTypes;
INSTANTIATE_TYPED_TEST_CASE_P(My, FooTest, MyTypes);

这种方案相比之前的方案提供更加好的灵活度,当然,框架越灵活,复杂度也会随之增加。

参考资料


覆盖率测试

为什么要测试代码覆盖率呢?想一想,你写了很多的代码,但是有一些很难测试到的地方,比如边界条件,如果这样未经测试的代码放入生产环境中是极端危险的,我们希望像下面这个图一样:

代码测试

得到每个文件有哪些行被测试到了,哪些行没有被测试到,测试到的行或者函数占比多少。

话不多说,我们赶快开始。

完整演示项目Github地址:CMakeGcovSupport

因为项目是用了cmake,所以我结合cmake进行讲解,新建一个CMakeLists.txt

  1. 首先需要在cmake中进行设置
OPTION (ENABLE_COVERAGE "Use gcov" OFF)
MESSAGE(STATUS ENABLE_COVERAGE=${ENABLE_COVERAGE})
IF(ENABLE_COVERAGE)
    SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage")
    SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fprofile-arcs -ftest-coverage")
ENDIF()

最重要的是标志位-fprofile-arcs -ftest-coverage 没有它就不能测试覆盖率!

2. 编译文件时指定设置标志位

mkdir build
cd build
cmake -DENABLE_COVERAGE=ON ..
make

3. 执行cmake编译后,就能在build目录下看到.gcno文件。接下来为了避免未覆盖的源码文件的覆盖率丢失,我们需要对覆盖率进行初始化。用到的是lcov工具,没有下载的同学可以先安装

cd ${PROJECT_ROOT}
lcov -d build -z
lcov -d build -b . --no-external --initial -c -o initCoverage.info

这里的-d后面是编译文件夹,-b是 后是项目根目录,--no-external表示忽略外部文件,-c表示捕获覆盖率数据,-o 是输出info文件的位置。

lcov其它常用标志: -i表示初始化(等价于 -initial-rc SETTING=VALUE覆盖参数。

4. 之后正式获取覆盖率报告

lcov -d build -b . --no-external -c -o ${projectname}.info
genhtml -o ${projectname}CoverageReport --prefix=`pwd` initCoverage.info ${projectname}Coverage.info

就会在${projectname}CoverageReport目录下生成对应的html文件,我们用浏览器打开,就可以得到上面代码测试率的网页版。

如果是非本地机器,配置http服务,我们也可以将该目录下的index.html 拷贝到/var/www/html目录下,这样在本地浏览器输入http://{Remote_IP}就可以看到覆盖率报告。

https://www.cnblogs.com/coderzh/archive/2009/04/06/1426758.html玩转Google开源C++单元测试框架Google Test系列(gtest)之一 - 初识gtesthttps://www.cnblogs.com/coderzh/archive/2009/04/06/1426758.html玩转Google开源C++单元测试框架Google Test系列(gtest)之二 - 断言https://www.cnblogs.com/coderzh/archive/2009/04/06/1430364.html玩转Google开源C++单元测试框架Google Test系列(gtest)之三 - 事件机制https://www.cnblogs.com/coderzh/archive/2009/04/06/1430396.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值