C++单元测试Gtest+Stub攻略
C++单元测试Gtest+Stub攻略
前言
笔者环境为linux环境(deepin),以下均在此环境进行
环境搭建
Gtest源码链接
Stub源码链接
StubExt源码链接
Stub的使用方法在cpp-stub/README_zh.md中有讲解
StubExt的使用方法在 cpp-stub-ext/ README.md中有讲解
StubExt可支持Lambda表达式进行打桩写Gtest时如果想获取一个固定的返回值或者出参十分好用
搭建环境时如果不想下载Gtest源码可以直接使用Stub中提供的Gtest库
仔细阅读README中说明,查看Stub源码提供的Demo
stubext.h中需要引用stub.h,并且需要将stub.h中的private改为protected
写Gtest时只要引用
#include “gtest/gtest.h”
#include “stubext.h”
#include “addr_pri.h”
编写单元测试
gtest的使用
Gtest提供了几个不同case方法进行测试不同的用例。主要常见的有TEST/TEST_F及TEST_P宏的使用。在每个TestCase中可以通过断言提供的方法进行控制检查程序的预期走向是否是期望的结果,从而以此来判定程序的正确性。
TEST的第一个参数一般情况下使用类名或者文件名,第二个参数一般情况下写要测试的函数名,如果一个函数需要写多个测试用例第二个参数增加一些后缀区分
TEST(test_case_name, test_name){
//编写你的TestCase
//使用EXPECT_EQ,ASSERT_EQ等去判断TestCase的值是否和期望值一致
}
这里说下EXPECT_*和ASSERT_*的区别,EXPECT_*如果当前不成立会继续执行当前TestCase而ASSERT_*如果不成立会直接结束当前TestCase
TEST_F的第一个参数为TestBase第二个参数一般情况下写要测试的函数名,如果一个函数需要写多个测试用例第二个参数增加一些后缀区分
class TestTmp{
public:
void setA(int a){
this->a=a;
}
int getA(){
return a;
}
private:
int a;
};
class TestBase : public ::testing::Test
{
protected:
// 为测试准备数据对象
void SetUp() override
{
t.setA(10);
}
// 清除资源
void TearDown() override
{
}
TestTmp t;
};
TEST_F(TestBase,test1){
int b =t.getA();
EXPECT_EQ(10,b);
t.setA(99);
int c = t.getA();
EXPECT_EQ(99,c);
}
TEST_F(TestBase,test2){
int b =t.getA();
EXPECT_EQ(10,b);
}
输出如下:
通过输出可以看出在test1中调用的t.setA(99);在test2中并未生效,所以TEST_F只是通过自定义一个类实现了一些资源的准备,在各个TEST_F中都可以使用准备好的资源,如果在TEST_F中修改了准备好的资源也只对当前这个TEST_F有效.
TEST_P的参数和TEST_F类似
class TestTmp{
public:
void setA(std::string a){
this->a=a;
}
std::string getA(){
return a;
}
private:
std::string a;
};
class StringsFixture : public ::testing::TestWithParam<std::string>
{
public:
virtual void SetUp()
{
t.setA("test");
}
virtual void TearDown()
{
// Do some tear down
}
TestTmp t;
};
INSTANTIATE_TEST_SUITE_P(
aValue, // Instantiation name
StringsFixture, // Fixture controller
::testing::Values( // Parameters
"qwe",
"asd",
"zxc",
"bnm",
"tyu",
"iop"
)
);
TEST_P(StringsFixture,test1){
auto test_str = this->GetParam();
EXPECT_STREQ(t.getA().c_str(),"test");
t.setA(test_str);
EXPECT_STREQ(t.getA().c_str(),test_str.c_str());
}
TEST_P相当于对这个案例做了一个循环调用,参数值设置几个就会调用几次,每次调用可以通过this->GetParam();获取之前预制的参数值.如果有案例需要重复输入不同的参数去测试就可以使用这种方式来减少代码量.
Stub/StubExt的使用
Stub是通过改变运行是调用进程内存页,将原本调用的函数地址改为打桩函数的地址从而去满足打桩需求.
int fun()
{
int a=10;
LOGD("--------------------------------------------fun");
return a;
}
int fun_stub()
{
int a=99;
LOGD("--------------------------------------------stub");
return a;
}
TEST(test_case_name, test_name)
{
Stub stub;
stub.set(fun, fun_stub);
int ret = fun(); //调用fun就去执行fun_stub中的代码
EXPECT_EQ(ret, 99);
stub.reset(fun);
ret = fun(); //此时调用fun就执行fun中的代码
EXPECT_EQ(ret, 10);
}
TEST(test_case_name, test_name1)
{
StubExt stub;
int dd = 0;
stub.set_lamda(fun, [&dd](){
dd = 55;
return dd;
});
int ret = fun(); //调用fun就去执行fun_stub中的代码
EXPECT_EQ(ret, 55);
}
Stub只可以通过桩函数fun_stub实现打桩,StubExt可以通过set设置桩函数也可以通过set_lamda设置lambda表达式并且如果你不关注返回值和参数可以统统都不设置让设置的函数执行个空相当于被打桩函数被跳过.更加详细的使用方式请参考README和doc_zh.md
注意事项:
Stub::set()和Stub::reset()一定要成对出现除非你想在后面执行的程序都走到桩函数中去
addr_pri.h中定义了获取私有成员私有方法的宏这里简单列举,具体的可以到README和doc_zh.md中通过下面的关键字搜索
Declaration:
ACCESS_PRIVATE_FIELD(ClassName, TypeName, FieldName)
ACCESS_PRIVATE_FUN(ClassName, TypeName, FunName)
ACCESS_PRIVATE_STATIC_FIELD(ClassName, TypeName, FieldName)
ACCESS_PRIVATE_STATIC_FUN(ClassName, TypeName, FunName)
Use:
access_private_field::ClassNameFieldName(object);
access_private_static_field::ClassName::ClassNameFieldName();
call_private_fun::ClassNameFunName(object,parameters...);
call_private_static_fun::ClassName::ClassNameFunName(parameters...);
get_private_fun::ClassNameFunName();
get_private_static_fun::ClassName::ClassNameFunName();