GoogleTest进阶——参数测试、Mock测试、耗时测试、类型测试

1. 前情提要

GoogleTest是一个为 C++ 开发的单元测试框架,为书写单元测试提供了很多有利的轮子,可以较大程度上的避免为了书写 单元测试 而需要重复搭建轮子的困扰。

本系列文章之前有一篇入门的基础文档。主要偏重于GoogleTest模块的环境搭建和简单的知识讲解,包括但不限于:

1. GoogleTest与GoogleMock环境搭建
2. TEST,TEST_F,MOCK_METHOD三个轮子的简单用例介绍
3. EXPECT_EQ、EXPECT_LE、EXPECT_GT、EXPECT_STREQ的介绍
4. ASSERT_*系列介绍

若对以上知识要点并不清楚,可以借用之前文章的传送门,进行了解学习,具体连接如下:

GoogleTest入门——从搭建到主要功能案例介绍

2. 本次文章的重点

本次文章以一个真实业务中的进程管理类测试的过程中遇到的问题和解决办法,覆盖了参数测试Mock接口测试接口耗时测试这三个方面,会简单的提及类型测试。并且配合VSCode的CMake Tools插件,快捷而方便的进行进程管理类的测试。

3. 写单元测试之前需要清楚的思路

要单元测试要进行之前,一定要明确每个用的的测试目标是什么,例如是为了测某个类的实现是否正确,还是为了测试某个外部调用这个类的方式是否正确。在明确了这一点后,就可以进开始开干单元测试了。而真正第一步还并不是就起手就开始写测试用例,而是需要在脑中画出如下的调用关系图。

GoogleTest进阶——参数测试、Mock测试、耗时测试、类型测试_第1张图片

所有的程序几乎都是这样的架构,最顶层的入口一定是main函数,而且是一个全局唯一的入口。main函数中直接调用了各种自己写的类,或者框架,或者STL等等的类接口。然后整个程序的代码中,总有一些接口是会调用 System API 的(系统接口)。

这个时候,我们写测试代码的思路和程序运行的方向刚好是反的。在理清整个程序的调用关系树状图后,我们需要从最底层往上写测试用例,并且每次测试的均为某一段箭头(本质就是某一个函数调用的具体实现)。

测试的过程中,对于调用 System API 的接口,则可以直接通过Mock一个测试桩的方式,待某个测试用例跑完后,查看测试桩中是否按照预定传入了 对应的参数 。Mock的测试桩也可以预定一些返回值,以达到测试过程并不受到真实 System API 的影响和测试环境制约,但同样可以测试的目的。

其他的上层依赖接口,例如图例中的 复杂依赖接口,则同样可以仅测试一个接口实现,而把下层已经测试过具体实现的底层接口通过新增底层Mock测试桩的方式达到经测试单个接口实现的单元测试效果。

4. 先从 CMakeLists.txt 的注意要点开始讲起

4.1 CMake Tools的安装

如果IDE使用的是VSCode,那么可以是用CMake Tools配合进行单元测试,会有更加丝滑的体验。

GoogleTest进阶——参数测试、Mock测试、耗时测试、类型测试_第2张图片

这里安装好后,需要在VSCode中修改对应的配置。这里直接 Ctrl + Shift + P,搜索 settings,然后进入配置,在配置文件中新增如下内容,为后续方便,本文贴出自己使用的settings json文件,以供参考Settings.json:

"cmake.defaultVariants": {
    "buildType": {
        "default": "500",
        "description": "The build type.",
        "choices": {
            "300": {
                "short": "300",
                "long": "编译300信号机可执行程序",
                "settings": {
                    "TSC": "300",
                    //"DEBUG":"DEBUG"  打开启动DEBUG模式
                },
                "buildType": "Release"
            },
            "500": {
                "short": "500",
                "long": "编译500信号机可执行程序",
                "settings": {
                    "TSC": "500"
                    //"DEBUG":"DEBUG"  打开启动DEBUG模式
                },
                "buildType": "Release"
            },
            "400": {
                "short": "400",
                "long": "编译400信号机可执行程序",
                "settings": {
                    "TSC": "400"
                    //"DEBUG":"DEBUG"  打开启动DEBUG模式
                },
                "buildType": "Release"
            },
            "pc": {
                "short": "pc",
                "long": "编译pc可执行程序",
                "settings": {
                    "TSC": "pc"
                    //"DEBUG":"DEBUG"  打开启动DEBUG模式
                },
                "buildType": "Release"
            },
            "debug": {
                "short": "Debug",
                "long": "Disable optimizations - include debug information.",
                "buildType": "Debug"
            },
            "release": {
                "short": "Release",
                "long": "Optimize for speed - exclude debug information.",
                "buildType": "Release"
            },
            "pc_test": {
                "short": "pc_test",
                "long": "ctest编译pc可执行程序",
                "settings": {
                    "TSC": "pc_test",
                    "CTEST": "true"    //这个环境变量比较有用,在后续的CMakeLists中会进行联动。
                    //"DEBUG":"DEBUG"  打开启动DEBUG模式
                },
                "buildType": "Release"
            },
        }
    }
},

完成设置后,点击最下面的CMake Tools控件,即可以发现我们新设置的Build Variant。如下图所示。

GoogleTest进阶——参数测试、Mock测试、耗时测试、类型测试_第3张图片

4.2 单元测试经常遇到的问题————如何测试类的私有成员变量,但又不想修改业务类代码新增get接口

gcc系列编译器支持一个比较好的功能,-fno-access-control,编译的时候增加这个功能。即可将生成的目标文件中所有privateprotected权限的访问限制直接变成public不用修改代码

加了这个编译选项后,可以直接通过类对象访问私有成员变量。是不是很 COOOOOOOL !

对应CMakeLists.txt中,可以新增如下字段,即可完成

if(DEFINED CTEST)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-access-control")
    add_definitions(-DUSE_CTEST)
endif()

同样,为了测试代码需要新增的头文件路径也可以通过以下方式进行:

if(DEFINED CTEST)
    set(HEADERS
    ${HEADERS} 
    ./unittest/include )
endif()

当然编译测试代码的时候可能你并不像编译原业务组件,也可以通过如下方式进行排除:

if(NOT DEFINED CTEST)
    add_executable(业务组件名称 ${ALL_SRCS})
    target_link_libraries(业务组件名称 ${LIBRARIES})
endif()

5. Mock测试

5.1 针对测试主体进行分类,明确哪些需要Mock

最上面谈到的程序接口逻辑关系图中,我们提到测试代码要从最下面开始测。然后主要遇到的接口类型会是以下几种。

a. 对于 无任何依赖接口 的测试则相对简单,主要关注整个测试过程中的 入参返回值 是否符合预期即可。对于类成员函数而言,配合 -fno-access-control 直接探测内部成员变量是否按照预期变更即可。

b. 对于 仅依赖系统API接口 的情况稍显复杂,需要在 a 步骤的基础上,新增一些系统接口的测试桩。例如,有些接口需要从串口或者网络中读取一些特定的字符才能出现。那么这种接口,直接使用系统API并不一定能测试。那么就需要手动制作特定的测试桩。

c. 对于 复杂依赖接口 的情况更加复杂一些。不光是涉及到的系统API需要自己打桩,对于已经测过的底层依赖API也需要进行打桩。这样保证每个测试用例测试的都是单个的接口实现。当然,对于已经测试过的接口直接用真实接口调用也可以。那么后续的值预测和接口调用预测则需要考虑更深层次的范围。

5.2 Mock其他模块的好处是什么

这里先针对本次测试过程中的依赖外部日志模块的接口进行打桩,由于本次测试的主体是进程管理模块,那么我们并不关心日志模块。

Mock其他模块的好处在于以下几点:

a. 其他模块的异常,段错误,abort等问题不会影响到自己模块。

b. 即便是其他模块没有开发完,只要接口告知和与其效果告知,并不会影响开发和测试。

c. 能够清晰的展示自己和外部的逻辑关系。

5.3 本次测试中Mock的其他模块和系统模块MockAPI示例

以下是Mock的日志模块示例,GoogleTest提供了GMock这个轮子,使得写MockAPI的时候,并不需要实现API的功能,简单的写一行就完成了Mock测试桩的制作。后续MockAPI的期待返回,期待入参约束在具体用例中也可以很容易的约束。

#pragma once
#include "gtest/gtest.h"
#include "gmock/gmock.h"

class MockLog
{
public:
    MOCK_METHOD(void, info, (int, std::string, bool, std::string, const char*));
    MOCK_METHOD(void, error, (int, std::string, bool, std::string, const char*));
};

以下是系统MockAPI的示例,具体描述不展开。这里需要说明的是,最新的Googletest 1.10版本,MOCKE_METHOD已经去掉了要求写n的方式。整体MockAPI声明的写法更加自然,更加容易。这一点书写的流畅度和以往比来有较大的提升。

#pragma once
#include "gtest/gtest.h"
#include "gmock/gmock.h"
#include 
#include 

class MockSystemAPI
{
public:
    MOCK_METHOD(void, sleep_for, (std::chrono::seconds));
    MOCK_METHOD(int, kill, (pid_t, int));
    MOCK_METHOD(pid_t, waitpid, (pid_t,int *, int));
    MOCK_METHOD(int, execvpe, (const char * ,char *const *, char *const *));
    MOCK_METHOD(pid_t, fork, ());
};

6. 参数测试

之前的介绍文章中讲解过测试夹具(TEST_F),这里的参数测试实际上是包含测试夹具的所有功能,他们直接是is a关系。具体不了解测试夹具的,可以移步学习 GoogleTest入门——从搭建到主要功能案例介绍。

6.1 参数测试有别于测试夹具的地方

这里参数测试新增的东西主要是一个自动生成正交参数集的功能。

我们给出一个场景,例如一个接口有5个入参。

int test(int a, int b, int c, int d, int e);

这里我们的所有入参的约束不一样。例如,a的入参可能是3种可能,1,0,-1,b的入参可能范围更多,从0-100。其他后面的cde也有各自的要求。

这里参数测试给了一个自动正交组合的功能。当约束好abcde参数的范围后,GoogleTest会自动按照每个参数的可能性正交遍历生成所有的搭配组合,并每个组合依次测试。

6.2 参数测试的参数准备阶段

大概写好的例子就是这样的:

首先限定好每个入参的可使用范围:

// delay延迟启动参数的测试集
// 这里后续直接改用::testing::Range自动生成
// const std::initializer_list delayset = {0,1,2,3,4,5,6};      
// 是否重启参数的测试集
const std::initializer_list<bool>  boolset = {true, false};  
// destory的测试用例集
const std::initializer_list<bool> destoryset = {true, false};
// fork返回pid值的测试用例集
const std::initializer_list<int> pidset = {987, 660, 562, 0, -1};              
// argvec的测试用例集
const std::initializer_list<std::list<std::string>> argset = {
    std::list<std::string>{std::string("--webdir"), std::string("/dev/shm")},
    std::list<std::string>{std::string("--lastpid"), std::string("0"), std::string("--webdir"), std::string("/dev/shm")},
    std::list<std::string>{std::string("--lastpid"), std::string("980"), std::string("--webdir"), std::string("/dev/shm")},
    std::list<std::string>{std::string("--webdir"), std::string("/dev/shm"), std::string("--lastpid"), std::string("980")},
    std::list<std::string>{std::string("--baseport"),std::string("30000"), std::string("--debug")},
    std::list<std::string>{std::string("--baseport"),std::string("30000"), std::string("--debug"), std::string("--lastpid"), std::string("980")}
};

然后调用框架的参数搭配生成接口:

INSTANTIATE_TEST_SUITE_P(P1, SubProject_Cons,\
    testing::Combine(testing::Values(std::string("progarm1")),testing::Values(std::string("1.0.1")), testing::Range(0,7,1), \
                    testing::Values(9),testing::Values(2), testing::Values(2), \
                    testing::Values(std::list<std::string>()), testing::ValuesIn(argset), \
                    testing::Values(std::list<std::string>()),\
                    testing::ValuesIn(boolset), testing::ValuesIn(destoryset), testing::ValuesIn(pidset)));

这样搭配生成后,框架会自动将参数进行正交生成约定的参数测试数据集。

然后我们使用一个约定好的类命继承这个数据集即可,这里要求类名必须是INSTANTIATE_TEST_SUITE_P传入的第二个参数名字,并且必须要继承public::testing::TestWithParam。对于第一个参数,实际上无所谓,只要不重复即可。本次用例的代码是这样的:

class SubProject_Cons : public::testing::TestWithParam<::testing::tuple<std::string,\
                                     std::string, int,int,int,int,std::list<std::string>, \
                                     std::list<std::string>,std::list<std::string>, bool, \
                                     bool, int>>
{
    // 接口耗时
    enum { MAX_TIME_OUT = 20};
protected:
    void SetUp() override
    {
        
        std::tie(name.program, name.version, name.delay, name.killSignal, 
        name.checkTime, name.maxTimeout, name.comList, name.argvec, 
        name.env, boot, destory, pid_return) = GetParam();
        mk= MockSubProject(std::move(name), mklogger, mockSystemAPI);
        // 此变量用于测试用例耗时
        _curTimePoint = std::chrono::steady_clock::now();
    }
    // 这里必须提前显式释放,否则生成的子进程会在程序终止的时候异常结束
    void TearDown() override
    {
        mk.Release();
        auto _now = std::chrono::steady_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(_now
        - _curTimePoint);
        EXPECT_LT(duration.count(),MAX_TIME_OUT) << "时间超了呀,这时间太长了呀!"; //超时时报错
    }
private:
    monitor::RpcConf    name;
    MockLog             mklogger;
    MockSubProject      mk;
    MockSystemAPI       mockSystemAPI;
    bool                boot = false;
    bool                destory = false;
    pid_t               pid_return = -1;

    // 耗时测试需要变量
    std::chrono::steady_clock::time_point _curTimePoint;
};

完成了上述的代码实际上做到的是后续每一个基于SubProject_Cons的测试用例,都能自动测试840个参数搭配,是不是也很COOOOOL!。

6.2.1 简单的重温一下TEST_P的加载机制

基于参数测试的用例首先会查找对应参数测试宏中的第一个传参当做类名,查找对应类,然后使用这个类中的SetUp接口,然后在测试用例结束的似乎,调用这个类的TearDown接口。

那么针对上面的例子,每一个基于SubProject_Cons的测试用例,最开始的时候,都会将GetParam()返回的参数进行解包,然后复制给各个成员变量。在SetUp的最后,会记录这个用例开始的时刻点。

在用例完成的时候,会运行SubProject_ConsTearDown接口,这里主要作用是显示释放测试资源,并且再记录一个时刻点与SetUp中的比较,从而达到耗时测试的目的。在耗时测试的最后,有一个EXPECT_LT接口,要求用例耗时必须小于一个设定值,否则认为测试失败。

6.3 参数测试的用例书写阶段

准备好了参数测试的参数搭配前置后,就可以进行测试用例书写了。当进入测试用例的第一行代码前,实际上已经执行了SubProject_ConsSetUp函数,所以可以理解成,这里的参数已经按照之前的预设的某一个方案填好了。

然后这里需要讲解一个重要的知识点:

EXPECT_CALL

这个约束有点特殊,需要注意的事情这个期望是需要先设置期望,然后再进行动作。期望之前执行的动作并不能被算入期望值中。

掌握清楚以上这个要点,是使用好 EXPECT_CALL 这个功能的先决条件。

我们用下面这个例子来具体讲解一下。

//测试SubProject::onConfUpdate
TEST_P(SubProject_Cons, onConfUpdate) {
    // 设置期望调用次数
    EXPECT_CALL(mklogger, error)
    .Times(0);
    mk._res->onConfUpdate();
    // 测试业务期望值,onConfUpdate
    EXPECT_EQ(mk._res->total_arg.size(), name.argvec.size() + 1) << "onConfUpdate 组参数个数不正常!";
    EXPECT_EQ(mk._res->total_arg.front(), name.program) << "onConfUpdate total_arg组参数不正常!";
    mk._res->total_arg.pop_front();
    // 这里是由于std::list<> 是支持直接==操作比较的,==操作stl重载为逐元素对比。
    EXPECT_EQ(mk._res->total_arg, name.argvec) << "onConfUpdate argvec组参数不正常!";;
};

这里的

EXPECT_CALL(mklogger, error)
.Times(0);

表示的是,从这行代码开始到用例结束的时候,这整个过程中,一次mklooger对象的error接口都不能调用。这句话的潜台词表示的是,EXPECT_CALL虽然这个用例执行了 SetUp 接口,但是这个接口中即便涉及到了mklooger对象的error接口,也不会被计算进去。

理解以上这个概念尤为重要。另外一个重要的关注点在于

这里尤其需要注意,对于EXPECT_CALL的期望,同样的接口如果在期望观察的时候,前面有调用但是没关注到,会产生报错,所以如果观察某个接口,那么必须将这个接口的全调用路径顺序都定位清楚。

7. 耗时测试

详见 6.2 参数测试的参数准备阶段,这是一个简单的耗时测试例子,但是往往耗时测试关注的不是单次结果,而是一个统计值。这样的场景下,GoogleTest也是能覆盖住的。

7.1 自定义测试输出

测试用例的调用结果,往往具有一定的价值,可以输出系统的调用性能报告,默认的输出中提供了调用开始,结束,成功,失败,耗时等一系列的信息,通过自定输出的方式,可以帮助我们更有针对性的获取一些我们想要的数据。

应用示例:

还是rpc调用测试用例,在这个用例中,我们运行1000+轮,想要获取每个接口的最小耗时,最大耗时和平均耗时,默认的输出信息难以进行数据的提取和分析,
这种情况下可以使用googletest提供的事件处理类,进行自定义输出。

//继承gtest的事件监听类
class PrintResult : public testing::EmptyTestEventListener
{
	struct result
	{
		std::string name;
		int successed = 0;
		int failed = 0;
		int maxtime = 0;
		int sumtime = 0;
		result(const std::string &_name) : name(_name) {}
		result(const char *_name) : name(_name) {}
	};
	
private:
	//在测试套件运行结束时进行统计
	void OnTestSuiteEnd(const testing::TestSuite& test_suite) override
	{
		//只统计Rpc调用这个TestSuit
		if (strcmp(test_suite.name(),"RpcTimeTakeTest") != 0) {
			return ;
		}
		for (int j = 0; j < test_suite.total_test_count();++j) {
			const testing::TestInfo &test_info = *test_suite.GetTestInfo(j);
			std::string name(test_info.name());

			if (infos.find(name) == infos.end()) {
				result n(name);
				infos.emplace(name,n);
			}
			auto &r = infos.find(name)->second;
			const testing::TestResult &ret = *test_info.result();
			if (ret.Passed()) {
				r.successed++;
			} else if (ret.Failed()) {
				r.failed++;
			}
			if ((int)ret.elapsed_time() > r.maxtime) {
				r.maxtime = (int)ret.elapsed_time();
			}
			r.sumtime += (int)ret.elapsed_time();
		}
	}
	//在程序结束时进行打印
	void OnTestProgramEnd(const testing::UnitTest& unit_test) override
	{
		//
		printf("------------------调用统计情况----------------\n");
		for (auto it : infos) {
			auto &&r = it.second;
			printf("[%-16s] ",r.name.c_str());
			printf("succed[%4d] ",r.successed);
			printf("failed[%4d] ",r.failed);
			printf("aveTimeTake:%2dms ",r.sumtime/(r.successed+ r.failed));
			printf("maxTimeTake:%2dms\n",r.maxtime);
		}
	}
	std::map infos;
};


::testing::TestEventListener* createUserListener() { return new PrintResult;}

在main函数中添加这个监听对象


::testing::TestEventListener* createUserListener();
int main(int argc, char *argv[])
{
	::testing::InitGoogleTest(&argc, argv);

	//添加我们自己的EventListener
	testing::UnitTest &unit_test = *testing::UnitTest::GetInstance();
	testing::TestEventListeners& listeners = unit_test.listeners();
	listeners.Append(createUserListener());

	return  RUN_ALL_TESTS();
}

执行我们的用例

./RpcTest --gtest_random_seed=0 --gtest_shuffle --gtest_repeat=1000

在运行结束后,输出如下信息

------------------调用统计情况----------------
[addRoad         ] succed[1000] failed[   0] aveTimeTake: 0ms maxTimeTake: 2ms
[configUpdate    ] succed[1000] failed[   0] aveTimeTake: 0ms maxTimeTake: 1ms
[currentRoad     ] succed[1000] failed[   0] aveTimeTake: 0ms maxTimeTake: 1ms
[delRoad         ] succed[1000] failed[   0] aveTimeTake: 0ms maxTimeTake: 1ms
[downGrade       ] succed[1000] failed[   0] aveTimeTake: 0ms maxTimeTake: 2ms
[getLeftRule     ] succed[1000] failed[   0] aveTimeTake: 0ms maxTimeTake: 4ms
[getRtChannel    ] succed[1000] failed[   0] aveTimeTake: 0ms maxTimeTake:15ms
[getRule         ] succed[1000] failed[   0] aveTimeTake: 0ms maxTimeTake: 1ms
[isTransition    ] succed[1000] failed[   0] aveTimeTake: 0ms maxTimeTake: 1ms
[lock            ] succed[1000] failed[   0] aveTimeTake: 0ms maxTimeTake: 1ms
[lockLeft        ] succed[1000] failed[   0] aveTimeTake: 0ms maxTimeTake: 1ms
[lockStatus      ] succed[1000] failed[   0] aveTimeTake: 0ms maxTimeTake: 1ms
[phaseCtrl       ] succed[1000] failed[   0] aveTimeTake: 0ms maxTimeTake: 3ms
[setCycle        ] succed[1000] failed[   0] aveTimeTake: 0ms maxTimeTake: 1ms
[setRule         ] succed[1000] failed[   0] aveTimeTake: 0ms maxTimeTake:13ms

如此:

我们统计出1000次调用中,成功的次数,失败的次数,最大耗时和平均耗时,通过自定义的输出信息,很容易帮助我们检查出系统中耗时较高的接口,方便我们进行针对性的优化。

8. 类型测试

理解了参数测试后,其实类型测试和参数测试的整体流程极为相似,只是把参数自动生成换成了类型自动生成。生成的范围和宏声明有些许变化而已,以下用一个例子说明:

//相位实际是同一种类型,只是状态不同,我们要将他抽象成不同的类型,需要简单封装一下
enum
{
	PHASE_TYPE_NOTRUN = 0,
	PHASE_TYPE_RUNNING,
	PHASE_TYPE_OVER,
	PHASE_TYPE_STAYING,
};

// 这里是待测试的PhaseBase类,是一个模板类,所以需要用类型测试来覆盖几个目标类型
template <int N>
class PhaseBase
{
 public:
    PhaseBase()
    {
        /*构造函数生成不同状态的相位*/
    }
    static const int type = N;
	Phase ph;
};


// 通过简单的封装,我们有了具有不同类型的相位
// 使用gtest接口进行类型声明

// 这里从意思上等效于设置一个类型集,和上文的参数集是一样的。
using 	testing::Types;
typedef Types<PhaseBase<PHASE_TYPE_NOTRUN>,
				PhaseBase<PHASE_TYPE_RUNNING>,
				PhaseBase<PHASE_TYPE_OVER>,
				PhaseBase<PHASE_TYPE_STAYING>> implatePhases;


//声明一个基于类型的测试类(测试夹具)
template <typename T>
class TypeTestSuit : public testing::Test
{
public:
	T phase;
};


//声明测试夹具TypeTestSuit 所要测试的类型 implatePhases

// 这里从意思上等效于参数测试里的INSTANTIATE_TEST_SUITE_P
TYPED_TEST_SUITE(TypeTestSuit,implatePhases);

//不同的相位,只需要编写一个reset用例
// [reset] 任何状态下的相位都可以被reset,reset后的状态都是确定的
TYPED_TEST(TypeTestSuit,reset)
{
	this->phase.ph.reset();
	EXPECT_TRUE(this->phase.ph.notrun());
	EXPECT_FALSE(this->phase.ph.running());
	EXPECT_FALSE(this->phase.ph.over());
	EXPECT_FALSE(this->phase.ph.staying());
}
/*
	上面这个用例,会将 implatePhases 中的不同状态相位都执行一遍 reset 这个 TEST
	当我们引入一种新的类型的相位时,只需要添加到implatePhases中去,不需要重新写用例
*/

9. GoogleTest 成果物运行的使用技巧

9.1 随机的调用顺序

测试用例和实际应用中一个差别在于,我们的用例往往是顺序执行的,不能有效的模拟实际的应用场景,对于这种情况,我们可以使用gtest的命令行参数,来随机的执行用例。

应用示例:

测试rpc接口的用例是按编写的顺序执行的,每次执行用例都是相同的顺序,这种情况与实际应用并不相符

为了更好的模拟随机调用这种场景,我们只需要在执行用例时,简单的加上命令行参数即可

testProcess --gtest_random_seed=0 --gtest_shuffle

9.2 多轮测试

在一些测试中,单次的测试可能并不具备代表性,特别是一些存在统计意义的测试,同时,上一节中的随机测试,单次运行也不具备代表性,对于此类场景,我们可以外部再封装一层脚本,将用例执行多次,
另外的,googletest也给我们提供的了现成的方法

应用示例:

在rpc 调用的测试用例中,不仅有随机调用的需求,耗时测试也应该具有统计意义,单次调用不超时,不代表这个接口表现稳定。

对于这种测试需求,我们结合googltest的耗时用例,随机调用,多轮测试进行混合的测试

testProcess --gtest_random_seed=0 --gtest_shuffle --gtest_repeat=1000

此操作会将我们编写的用例,以随机的方式,运行1000次

多轮测试中一个常见的问题的是,我们默认编写的断言一般是EXPECT_ , 出现错误时不会退出,在多轮测试中的大量打印中,我们可能会错过出错的那条信息,对于这种情况,我们再增加命令行参数,在出错时直接中断

 --gtest_break_on_failure

Append vscode remote setting

vscode settings json:

{
    "breadcrumbs.enabled": true,
    "editor.mouseWheelZoom": true,
    "editor.renderWhitespace": "all",
    "c-cpp-flylint.flexelint.enable": false,
    "c-cpp-flylint.cppcheck.force": true,
    "c-cpp-flylint.cppcheck.language": "c++",
    "c-cpp-flylint.cppcheck.verbose": true,
    "editor.fontSize": 18,
    "C_Cpp.updateChannel": "Insiders",
    "c-cpp-flylint.cppcheck.platform": "unix64",
    "c-cpp-flylint.debug": true,
    "c-cpp-flylint.clang.blocks": false,
    "c-cpp-flylint.cppcheck.inconclusive": true,
    "C_Cpp.default.intelliSenseMode": "linux-gcc-arm",
    // "C_Cpp.default.systemIncludePath": [
    //     "/data1/xiaoyanyi/cross-tool/arm-imx6ul-linux-gnueabihf/arm-imx6ul-linux-gnueabihf/include/c++/5.4.0/**",
    //     "/data1/xiaoyanyi/cross-tool/arm-at91-linux-gnueabi/arm-at91-linux-gnueabi/include/c++/4.9.2/**",
    //     "/data1/xiaoyanyi/cross-tool/arm-imx6ul-linux-gnueabihf/arm-imx6ul-linux-gnueabihf/sysroot/usr/include",
    //     "/data1/xiaoyanyi/cross-tool/arm-at91-linux-gnueabi/arm-at91-linux-gnueabi/sysroot/usr/include",
    // ],
    "[cpp]": {
        "editor.quickSuggestions": true
            },
        "[c]": {
        "editor.quickSuggestions": true
            },
        "C_Cpp.default.includePath": [
            "/data1/xiaoyanyi/cross-tool/arm-imx6ul-linux-gnueabihf/arm-imx6ul-linux-gnueabihf/include/c++/5.4.0/**",
            "/data1/xiaoyanyi/cross-tool/arm-at91-linux-gnueabi/arm-at91-linux-gnueabi/include/c++/4.9.2/**",
            "/data1/xiaoyanyi/cross-tool/arm-imx6ul-linux-gnueabihf/arm-imx6ul-linux-gnueabihf/sysroot/usr/include",
            "/data1/xiaoyanyi/cross-tool/arm-at91-linux-gnueabi/arm-at91-linux-gnueabi/sysroot/usr/include",
            "/data1/xiaoyanyi/work/common/**",
            "${workspaceFolder}/**",
        ],
        "C_Cpp.default.cppStandard": "c++11",
        "C_Cpp.default.cStandard": "c99",
        "C_Cpp.intelliSenseEngineFallback": "Enabled",
        "C_Cpp.loggingLevel": "Debug",
        "explorer.confirmDelete": false,
        "workbench.colorTheme": "Monokai",
        "python.pythonPath": "/usr/bin/python3",
        "window.zoomLevel": 0,
        "C_Cpp.commentContinuationPatterns": [
            "/**"
        ],
        "markdown-preview-enhanced.revealjsTheme": "black.css",
        "markdown-preview-enhanced.automaticallyShowPreviewOfMarkdownBeingEdited": true,
        "markdown-preview-enhanced.previewTheme": "solarized-light.css",
        "hediet.vscode-drawio.local-storage": "eyIuZHJhd2lvLWNvbmZpZyI6IntcImxhbmd1YWdlXCI6XCJcIixcImN1c3RvbUZvbnRzXCI6W10sXCJsaWJyYXJpZXNcIjpcImdlbmVyYWxcIixcImN1c3RvbUxpYnJhcmllc1wiOltcIkwuc2NyYXRjaHBhZFwiXSxcInBsdWdpbnNcIjpbXSxcInJlY2VudENvbG9yc1wiOltdLFwiZm9ybWF0V2lkdGhcIjpcIjI0MFwiLFwiY3JlYXRlVGFyZ2V0XCI6ZmFsc2UsXCJwYWdlRm9ybWF0XCI6e1wieFwiOjAsXCJ5XCI6MCxcIndpZHRoXCI6ODI3LFwiaGVpZ2h0XCI6MTE2OX0sXCJzZWFyY2hcIjp0cnVlLFwic2hvd1N0YXJ0U2NyZWVuXCI6dHJ1ZSxcImdyaWRDb2xvclwiOlwiI2QwZDBkMFwiLFwiZGFya0dyaWRDb2xvclwiOlwiIzZlNmU2ZVwiLFwiYXV0b3NhdmVcIjp0cnVlLFwicmVzaXplSW1hZ2VzXCI6bnVsbCxcIm9wZW5Db3VudGVyXCI6MCxcInZlcnNpb25cIjoxOCxcInVuaXRcIjoxLFwiaXNSdWxlck9uXCI6ZmFsc2UsXCJ1aVwiOlwiXCJ9In0=",
        "remote.SSH.showLoginTerminal": true,
        "remote.SSH.remotePlatform": {
            "10.1.74.245": "linux"
        },
        "c-cpp-flylint.clang.enable": false,
        "c-cpp-flylint.flawfinder.enable": false,
        "c-cpp-flylint.lizard.enable": false,
        "eslint.format.enable": true,
        "remote.autoForwardPortsSource": "output",
        "markdown-preview-enhanced.enableScriptExecution": true,
        "markdown-preview-enhanced.codeBlockTheme": "github.css",
        "markdown-preview-enhanced.enableHTML5Embed": true,
        "markdown-preview-enhanced.HTML5EmbedIsAllowedHttp": true,
        "markdown-preview-enhanced.printBackground": true,
        "fileheader.customMade": {
            "Date": "Do not edit", // 文件创建时间(不变)
            "Author": "Adam Xiao",
            "LastEditors": "Adam Xiao", // 文件最后编辑者
            "LastEditTime": "Do not edit", // 文件最后编辑时间
            "FilePath": "Do not edit" // 文件在项目中的相对路径 自动更新
          },
        "fileheader.cursorMode": {
        // 默认字段
        "description":"",
        "param":"",
        "return":""
        },
        "markdown.preview.typographer": true,
        "cmake.statusbar.advanced": {
        },
        "clangd.path": "/data1/xiaoyanyi/cross-tool/clangd_13.0.0/bin/clangd",
        "cmake.cmakePath": "cmake",
        "cmake.defaultVariants": {
            "buildType": {
                "default": "500",
                "description": "The build type.",
                "choices": {
                    "300": {
                        "short": "300",
                        "long": "编译300信号机可执行程序",
                        "settings": {
                            "TSC": "300",
                            //"DEBUG":"DEBUG"  打开启动DEBUG模式
                        },
                        "buildType": "Release"
                    },
                    "500": {
                        "short": "500",
                        "long": "编译500信号机可执行程序",
                        "settings": {
                            "TSC": "500"
                            //"DEBUG":"DEBUG"  打开启动DEBUG模式
                        },
                        "buildType": "Release"
                    },
                    "400": {
                        "short": "400",
                        "long": "编译400信号机可执行程序",
                        "settings": {
                            "TSC": "400"
                            //"DEBUG":"DEBUG"  打开启动DEBUG模式
                        },
                        "buildType": "Release"
                    },
                    "pc": {
                        "short": "pc",
                        "long": "编译pc可执行程序",
                        "settings": {
                            "TSC": "pc"
                            //"DEBUG":"DEBUG"  打开启动DEBUG模式
                        },
                        "buildType": "Release"
                    },
                    "debug": {
                        "short": "Debug",
                        "long": "Disable optimizations - include debug information.",
                        "buildType": "Debug"
                    },
                    "release": {
                        "short": "Release",
                        "long": "Optimize for speed - exclude debug information.",
                        "buildType": "Release"
                    },
                    "pc_test": {
                        "short": "pc_test",
                        "long": "ctest编译pc可执行程序",
                        "settings": {
                            "TSC": "pc_test",
                            "CTEST": "true"
                            //"DEBUG":"DEBUG"  打开启动DEBUG模式
                        },
                        "buildType": "Release"
                    },
                }
            }
        },
        "cmake.statusbar.visibility": "compact",
        "cmake.enableTraceLogging": true,
        "clangd.checkUpdates": true,
        "cmake.parallelJobs": 8,
        "cmake.ctest.parallelJobs": 16,
        "jupyter.debugJustMyCode": false,
        "C_Cpp.workspaceSymbols": "All",
        "C_Cpp.autocomplete": "Disabled",
        "highlight-icemode.borderWidth": "3px",
        "highlight-icemode.borderRadius": "5px",
        "cmake.ctestPath": "",
}

Append 测试代码片段

#include "subproject.h"
#include "gmock/gmock-actions.h"
#include "gmock/gmock-matchers.h"
#include "gmock/gmock-spec-builders.h"
#include "gtest/gtest-param-test.h"
#include "gtest/gtest.h"
#include 
#include 
#include 
#include "mockLog.h"
#include "mockSystemAPI.h"

using ::testing::StartsWith;
using ::testing::InSequence;
using ::testing::Return;

using ::testing::AllOf;
using ::testing::Gt;
using ::testing::Eq;
using ::testing::Lt;
using ::testing::Ne;
using ::testing::FloatLE;
using ::testing::MatchesRegex;
using ::testing::StartsWith;
using ::testing::HasSubstr;
using ::testing::StrEq;

// delay延迟启动参数的测试集
// 这里后续直接改用::testing::Range自动生成
// const std::initializer_list delayset = {0,1,2,3,4,5,6};      
// 是否重启参数的测试集
const std::initializer_list<bool>  boolset = {true, false};  
// destory的测试用例集
const std::initializer_list<bool> destoryset = {true, false};
// fork返回pid值的测试用例集
const std::initializer_list<int> pidset = {987, 660, 562, 0, -1};              
// argvec的测试用例集
const std::initializer_list<std::list<std::string>> argset = {
    std::list<std::string>{std::string("--webdir"), std::string("/dev/shm")},
    std::list<std::string>{std::string("--lastpid"), std::string("0"), std::string("--webdir"), std::string("/dev/shm")},
    std::list<std::string>{std::string("--lastpid"), std::string("980"), std::string("--webdir"), std::string("/dev/shm")},
    std::list<std::string>{std::string("--webdir"), std::string("/dev/shm"), std::string("--lastpid"), std::string("980")},
    std::list<std::string>{std::string("--baseport"),std::string("30000"), std::string("--debug")},
    std::list<std::string>{std::string("--baseport"),std::string("30000"), std::string("--debug"), std::string("--lastpid"), std::string("980")}
};



class MockSubProject {
public:
    MockSubProject() = default;
    MockSubProject(monitor::RpcConf&& _name, MockLog& _mklogger, MockSystemAPI& _mkSystemAPI) \
                    : _res(monitor::SubProject::create(_mklogger, _name, _mkSystemAPI, true, false)) 
    {}
    MockSubProject(MockSubProject&&) = default;
    MockSubProject(const MockSubProject&) = delete;
    auto operator=(MockSubProject&&) -> MockSubProject& = default;
    auto operator=(const MockSubProject&) -> MockSubProject& = delete;
    ~MockSubProject() {Release();}

    void Release() {_res= nullptr;}
    
    std::shared_ptr<monitor::SubProject> _res;
};

class SubProject_Cons : public::testing::TestWithParam<::testing::tuple<std::string,\
                                     std::string, int,int,int,int,std::list<std::string>, \
                                     std::list<std::string>,std::list<std::string>, bool, \
                                     bool, int>>
{
    // 接口耗时
    enum { MAX_TIME_OUT = 20};
protected:
    void SetUp() override
    {
        
        std::tie(name.program, name.version, name.delay, name.killSignal, 
        name.checkTime, name.maxTimeout, name.comList, name.argvec, 
        name.env, boot, destory, pid_return) = GetParam();
        mk= MockSubProject(std::move(name), mklogger, mockSystemAPI);
        // 此变量用于测试用例耗时
        _curTimePoint = std::chrono::steady_clock::now();
    }
    // 这里必须提前显式释放,否则生成的子进程会在程序终止的时候异常结束
    void TearDown() override
    {
        mk.Release();
        auto _now = std::chrono::steady_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(_now
        - _curTimePoint);
        EXPECT_LT(duration.count(),MAX_TIME_OUT) << "时间超了呀,这时间太长了呀!"; //超时时报错
    }
private:
    monitor::RpcConf    name;
    MockLog             mklogger;
    MockSubProject      mk;
    MockSystemAPI       mockSystemAPI;
    bool                boot = false;
    bool                destory = false;
    pid_t               pid_return = -1;

    // 耗时测试需要变量
    std::chrono::steady_clock::time_point _curTimePoint;
};

// program  version   delay  
// killSignal   checkTime maxTimeout  
// comList argvec   env
INSTANTIATE_TEST_SUITE_P(P1, SubProject_Cons,\
    testing::Combine(testing::Values(std::string("progarm1")),testing::Values(std::string("1.0.1")), testing::Range(0,7,1), \
                    testing::Values(9),testing::Values(2), testing::Values(2), \
                    testing::Values(std::list<std::string>()), testing::ValuesIn(argset), \
                    testing::Values(std::list<std::string>()),\
                    testing::ValuesIn(boolset), testing::ValuesIn(destoryset), testing::ValuesIn(pidset)));


// std::string 				program;			//程序名(全路径)
// std::string					version;			//程序版本号
// int 						delay = 0;			//程序延时启动秒数
// int 						killSignal = 9;		//程序终止时候的信号量,默认为9,可以指定自定义信号量
// int 						checkTime = 0;		//程序单次最大超时响应时间
// int 						maxTimeout = 0;		//程序最大允许超时次数
// std::list		comList;			//程序待监听的服务端口列表
// std::list 		argvec;				//程序启动额外参数
// std::list		env;				//程序启动需要的环境变量

//测试SubProject::onConfUpdate
TEST_P(SubProject_Cons, onConfUpdate) {
    // 设置期望调用次数
    EXPECT_CALL(mklogger, error)
    .Times(0);
    mk._res->onConfUpdate();
    // 测试业务期望值,onConfUpdate
    EXPECT_EQ(mk._res->total_arg.size(), name.argvec.size() + 1) << "onConfUpdate 组参数个数不正常!";
    EXPECT_EQ(mk._res->total_arg.front(), name.program) << "onConfUpdate total_arg组参数不正常!";
    mk._res->total_arg.pop_front();
    // 这里是由于std::list<> 是支持直接==操作比较的,==操作stl重载为逐元素对比。
    EXPECT_EQ(mk._res->total_arg, name.argvec) << "onConfUpdate argvec组参数不正常!";;
};

// 测试SubProject::create
TEST_P(SubProject_Cons, create) {
    // 设置期望调用次数,EXPECT_CALL有个特性在于必须要在调用之前注册预测行为,然后进行调用
    // 所以最下面需要显式调用一次create,而所有的EXPECT_CALL要在真实调用之前注册。
    EXPECT_CALL(mockSystemAPI, fork()).WillOnce(Return(pid_return));
    if (pid_return == -1 ) {
        EXPECT_CALL(mklogger, error(ManagerEC::process_create,"子进程创建",false,testing::_, StrEq(name.program)))
        .Times(1);    
    } else if (pid_return == 0) {
        InSequence s;
        if (name.delay > 0 && boot) {
            EXPECT_CALL(mockSystemAPI, sleep_for(Eq(std::chrono::seconds(name.delay))))
            .Times(1);   
        }
        EXPECT_CALL(mklogger, info(ManagerEC::process_create,"子进程创建",true,testing::_, StrEq(name.program)))
        .Times(1);    
        EXPECT_CALL(mockSystemAPI, execvpe(StrEq(name.program),testing::_, testing::_))
        .Times(1);   
    } 
    
    mk._res->create(boot);
    // EXPECT_THAT和EXPECT_CALL不同,这里调用完后才会产生影响。
    if (pid_return > 0) {
        EXPECT_THAT(mk._res->get_pid(), Eq(pid_return)) << "子进程没有创建成功的呀" ;
    }
};

// 测试SubProject::destroy
TEST_P(SubProject_Cons, destroy) {
    // 启动的时候,应该没有pid值
    EXPECT_THAT(mk._res->get_pid(), Eq(-1)) << "子进程没有创建成功的呀";
    // 要在create之前设置好fork应有的返回值
    ON_CALL(mockSystemAPI, fork()).WillByDefault(Return(pid_return));
    // 先启动好,以防干扰后续判断,在这一步触发的mock并不会被下面的EXPECT_CALL记录
    mk._res->create(false);
    if (pid_return > 0) {
        EXPECT_THAT(mk._res->get_pid(), Ne(-1)) << "子进程没有创建成功的呀";
        if (destory) {
            InSequence s;
            EXPECT_CALL(mockSystemAPI, kill(mk._res->get_pid(),name.killSignal))
            .Times(1);
            EXPECT_CALL(mockSystemAPI, waitpid(mk._res->get_pid(), nullptr, 0))
            .Times(1);
            EXPECT_CALL(mklogger, error(ManagerEC::service_exited,"子服务退出",true,testing::_, StrEq(name.program)))
            .Times(1);  
        } else  {
            InSequence s;
            EXPECT_CALL(mockSystemAPI, kill(mk._res->get_pid(),name.killSignal))
            .Times(1);
            EXPECT_CALL(mockSystemAPI, waitpid(mk._res->get_pid(), nullptr, 0))
            .Times(1);
            EXPECT_CALL(mklogger, info(ManagerEC::service_exited,"子服务退出",true,testing::_, StrEq(name.program)))
            .Times(1);
        }
    } else if (pid_return <= 0) {
        EXPECT_CALL(mockSystemAPI, kill(testing::_,testing::_))
        .Times(0);
        EXPECT_CALL(mockSystemAPI, waitpid(testing::_, nullptr, 0))
        .Times(0);
        EXPECT_CALL(mklogger, error(ManagerEC::service_exited,"子服务退出",true,testing::_, StrEq(name.program)))
        .Times(0);  
    }
    mk._res->destroy(destory);
    EXPECT_THAT(mk._res->get_pid(), Eq(-1)) << "子进程没有注销成功的呀";
};

// 测试SubProject::reboot
TEST_P(SubProject_Cons, reboot) {
    // 启动的时候,应该没有pid值
    EXPECT_THAT(mk._res->get_pid(), Eq(-1)) << "子进程没有创建成功的呀";
    // 要在create之前设置好fork应有的返回值
    ON_CALL(mockSystemAPI, fork()).WillByDefault(Return(pid_return));
    // 先启动好,以防干扰后续判断,在这一步触发的mock并不会被下面的EXPECT_CALL记录
    mk._res->create(false);
    // destory的期望
    if (pid_return > 0) {
        EXPECT_CALL(mklogger, info(ManagerEC::service_exited,"子服务退出",true,testing::_, StrEq(name.program)))
        .Times(1);    
    }
    // create的期望
    if (pid_return == -1 ) {
        EXPECT_CALL(mklogger, error(ManagerEC::process_create,"子进程创建",false,testing::_, StrEq(name.program)))
        .Times(1);    
    } else if (pid_return == 0) {
        InSequence s;
        // 因为这里传入的是create false,所以不会执行,注意最后面的Times为0
        // if (name.delay > 0 && boot) {
        EXPECT_CALL(mockSystemAPI, sleep_for(Eq(std::chrono::seconds(name.delay))))
        .Times(0);   
        // }
        EXPECT_CALL(mklogger, info(ManagerEC::process_create,"子进程创建",true,testing::_, StrEq(name.program)))
        .Times(1);    
        EXPECT_CALL(mockSystemAPI, execvpe(StrEq(name.program),testing::_, testing::_))
        .Times(1);   
    } 
    // error传参为boot
    if (boot) {
        EXPECT_CALL(mklogger, error(ManagerEC::reboot,"子服务重启",true,testing::_, StrEq(name.program))).Times(1);    
    } else  {
        EXPECT_CALL(mklogger, info(ManagerEC::reboot,"子服务重启",true,testing::_, StrEq(name.program))).Times(1);    
    }

    mk._res->reboot(boot);
    // mk对象析构的时候会有一次退出
    // 以下EXPECT_CALL对应从reboot函数退出后,到mk析构后发生的事情。
    if (pid_return > 0) {
        EXPECT_CALL(mklogger, info(ManagerEC::service_exited,"子服务退出",true,testing::_, StrEq(name.program))).Times(1);    
    }
};


你可能感兴趣的:(C++,VSCode,单元测试,Gtest,Gmock,googletest,c++)