1. 什么是单元测试?
单元测试,维基百科给出定义:Unit Testing,又称为模块测试,是针对程序模块(软件设计的最小单元)进行正确性检验的测试工作。
2. 什么是模块?或者什么是最小单元?
通俗的说就是函数或者类的方法。
“单元”的定义,其实可以更加宽泛,在面向对象语言中,一个单元可以指一个方法,也可以是一个类。单元的选定更多的取决于我们测试的意图。
3. 为什么需要单元测试?
我们常说的单元测试,是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。简单来说,一个单元测试就是用于判断某个特定条件(或者场景)下某个特定函数的行为。
好处:开发人员可以通过这种方式,测试自己写的函数是否符合预期,在这个过程中,往往可以发现函数内部逻辑错误,带来优秀的代码治理和良好的异常处理以及完善错误报告。
4. 怎么写单元测试?或者怎么才算好的单元测试?
有明确的预期;
可重复运行;
没有副作用。
这里说到的副作用,即业务逻辑从外部依赖中解耦处理,比如说:时间、随机数、并发性、基础设置、持久化、网络等。这就需要我们对代码做更合理的划分,函数的职责更加的清晰。
但我们的往往有各种外部依赖,比如需要读写数据库,需要进行网络通信,需要操作设备等等,这就需要用到测试替身:Stub(桩)。桩的最大作用就是,不包含逻辑,返回固定数据。
5. 这里主要是记录一下C++的gmock的使用。
虽然gmock的入门看起来很简单,但是将其应用到具体的工程中,初学者又总会面临各种各样的问题,主要原因还是对于gmock是如何运作的存在误区,下面通过一个例子来看下,究竟gmock是如何替换某些函数方法的。
1)首先需要在C++工程中,设置附加包含目录和附加库目录为gtest和gmock的include和lib路径,附加依赖项中添加:gmock.lib和gtest.lib。需要注意的是,所使用的gmock.lib和gtest.lib与你所创建的C++工程,最好是一样的vs版本,即C++库版本一样,否则会出现一些意想不到的错误哦。
2)这里有一个test类,先假定它的作用就是没啥作用,就是给你看下:
正常来说,我们一般会这样写:
test.h:
class test{
public:
test();
~test();
int add(Dev* dev, int a);
};
假定Dev类是一个需要和设备进行交互的类,获取设备上的数据:
dev.h:
class Dev{
public:
Dev();
~Dev();
int getDevNum();
};
其中add方法的实现,假设就是一个加法运算,但是需要从某个设备中读取值上来与a进行计算:(假定我们这里的数值都必须是正数。)
test.cpp:
int test:add(Dev* dev, int a){
if(NULL == dev){
return -1;
}
if(a < 0){
return -2;
}
int sum = dev->getDevNum();//假定没有设备,函数返回-3
if(sum < 0){
return -3;
}
sum += a;
return sum;
}
我们需要对这个add方法做单元测试,但如果测试的环境没有设备,这个函数不就没法完全的测试了吗?
这个时候就需要借助gmock,来对getDevNum打桩,让getDevNum可以返回我们想让它返回的任何数。
需要对getDevNum进行打桩,还需要对Dev类的实现做更改,这是因为gmock的原理实际是依赖于C++的多态机制实现的,所以只有虚函数才能被mock,而非虚函数则无法mock。这一点也就要求我们在实现我们的类和方法时,要预先设计好,把这些依赖外部环境的实现分离开(不要与业务逻辑部分混合在一起,封装到单独的函数方法中),不然代码写一半再去使用gmock做单元测试,就往往需要对现有代码结构做很大的更改,造成很大的时间和精力浪费。
Dev类更改如下:
class Dev{
public:
Dev();
virtual ~Dev();
virtual int getDevNum();
};
将析构函数和getDevNum函数都设为虚函数,这样getDevNum函数就可以被mock了。
mock一下getDevNum:
首先我们在测试代码中定义一个MockDev类:
#include “gmock\gmock.h”
#include “dev.h”
class MockDev:public Dev{
public:
MOCK_METHOD0(getDevNum, int());
};
这样我们就成功mock了getDevNum函数了。
MOCK_METHOD0:固定写法,数字0表示mock的函数没有参数,如果有2个参数,就是MOCK_METHOD2,对应的4个参数就是MOCK_METHOD4;
参数1:就是我们需要mock的函数方法名称了;
参数2:是我们mock的函数指针类型,格式为:返回值类型(参数1,参数2...)
如果我们的getDevNum有两个形参:int getDevNum(int a, string b);那么这里的参数2格式为:int(int a, string b)
接下来就是我们的单元测试代码:
测试代码include包含我们刚刚创建MockDev的头文件,以及被测试代码的cpp:
#include “gtest\gtest.h”
#include “gmock\gmock.h”
#include “MockDev.h”
#include “test.cpp”
using namespace testing;
TEST(TestSuiteTest, add){
int ret;
MockDev mdev;
EXPECT_CALL(mdev, getDevNum()).WillRepeatedly(Return(1));
test t;
ret = t.add(NULL, 3);
EXPECT_EQ(-1, ret);
ret = t.add(&mdev, -1);
EXPECT_EQ(-2, ret);
ret = t.add(&mdev, 3);
EXPECT_EQ(4, ret);
}
这里EXPECT_CALL,就是说当调用mdev的getDevNum方法的时候,返回1.
当然还有很多丰富的返回,比如第一次调用返回1,第二次调用返回2等等,其他使用方法可以参考gmock的语法。
简单总结一下:
上面的例子虽然很简单,但是可以看出,
1)将需要mock的函数方法定义为虚函数;
2)我们需要在编写代码之初,将必要的接口分离,避免依赖外部环境的实现部分与业务逻辑部分混合。这是为了单元测试的时候,我们可以将依赖外部环境的函数实现进行mock。
其他:
实际工程中,我们一个类中可能不仅仅是共有的函数方法,可能还有一些私有的方法,对于这些私有的函数方法,应不应该测试呢?这个目前还没有统一的定论,需要看开发人员自己的意愿,这里给出私有方法测试的几种方法:
1)使用#define private public粗暴地将private变成public,需要将define放在#include头文件之前。如:
#define private public
#include “myclass.h”
2)使用friend。这个会相对友好一点,但是却需要修改原有的代码
3)将private方法定义为protected,然后在测试代码中继承,自己定义测试的共有方法,再调用父类中的private方法:
class Num{
protected:
int add(int a, int b);
};
//测试代码
class TestNum:public Num{
public:
int testAdd(int a, int b);
};
int TestNum:testAdd(int a, int b){
return add(a, b);
}
缺点也很明显,还是需要更改一下原代码。
本文为作者原创,如需转载,请在评论区征得作者同意,原创链接:https://blog.csdn.net/anranjingsi/article/details/106084223