单元测试其一:gt
在公司需要进行单元测试的任务(单元测试可以帮助了解你团队的项目),它包括以下几个部分:
- gtest单元测试
- gmock集成测试
- lcov代码覆盖率
因为要学的内容比较多,我将记录一些关键步骤,好让大家快速上手。
Gtest
googletest 是测试技术团队根据 Google 的特定要求和约束条件开发的测试框架。 无论您是在 Linux、Windows 还是 Mac 上工作,如果您编写 C++ 代码,googletest 都可以为您提供帮助。 它支持任何类型的测试,而不仅仅是单元测试。
https://google.github.io/googletestgoogle.github.io/googletest
GoogleTest User’s Guidegoogle.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 assertion | Nonfatal assertion | Verifies |
---|---|---|
ASSERT_TRUE(condition); | EXPECT_TRUE(condition); | condition is true |
ASSERT_FALSE(condition); | EXPECT_FALSE(condition); | condition is false |
数值比较
Fatal assertion | Nonfatal assertion | Verifies |
---|---|---|
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 assertion | Nonfatal assertion | Verifies |
---|---|---|
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 assertion | Nonfatal assertion | Verifies |
---|---|---|
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 assertion | Nonfatal assertion | Verifies |
---|---|---|
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行,那肯定不行。即使你愿意,但你的手可能不愿意。所以我们需要进行参数化,即指定一个参数列表就。步骤如下:
- 告诉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
- 定义一个模板类,继承`
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
- 首先需要在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