BlueTest¶
测试工程师使用的标准测试库。包含接口、压力、UI测试相关一站式解决方案
要求¶
- `Python`_ 3.5.3+
- `Request`_ 2.0.0+
Python下载路径 https://www.python.org/downloads/
Request下载路径 https://pypi.org/project/requests/
or 使用pip命令 pip install requests
安装¶
最简单的安装方式就是使用``pip``命令,作为一个pythoner,``pip``是必备工具之一
pip install BlueTest
pip3 install BlueTest # 双python环境 python3 pip
试一下,傻瓜式的Demo
>>>import BlueTest
>>>BlueTest.test() #接口基础测试DEMO
>>>BlueTest.presstest() #接口压测DEMO
- INFO: 测试数据生成 .//srcdata//test.json.postman_collection
- INFO: postman转csv成功:./srcdata/test.csv
- DEBUG: CSV文件内容序列化成功:[{'Lv': '', 'Cname': '', ...
- INFO: log exceptionCheck: 普通请求 ...
- INFO: log exceptionCheck: ['date']为空 ...
- INFO: log exceptionCheck: ['date']不传 ...
项目结构¶
│ test.py
│
├─log
│ all.log
│ error.log
│
├─result
│ data.txt
│ Press_1.txt
│ Press_2.txt
│ resualt.csv
│ time.csv
│
└─srcdata
test.csv
test.json.postman_collection
test.py测试脚本,请自行创建
log
,result
,srcdata
3个目录由BlueTest自行生成,用户无需关心
log
日志文件夹,
all.log
全部日志,隔天会自动重建并归档,error.log
错误日志
result
执行结果文件夹
data.txt
接口基本测试结果,Press_x.txt
压力测试原始数据 ,resualt.csv
time.csv
压力测试统计后结果srcdata
测试入口数据
test.json.postman....
POSTMAN导出文件 使用BluetTest.test()会自动创建一个demo,正式使用时需要用户自行添加test.csv
根据test.json.postman....
生成的中间文件
PS:之所以使用csv格式为转换和统计压力测试结果,是为了兼容不同的操作系统。而且便于后期的图表生成
Table of Contents¶
Quickstart¶
接口测试,是这个测试脚手架被搭建的初衷。所以代码的没有做到小而美。 秉承着一贯的原则,还是先看我们的范例吧
范例¶
接口基础测试
postman_collection
测试数据放置于 ./srcdata/
目录中
>>>import BlueTest
# xxx为测试文件test.json.postman_collection 名称。BlueTest.initPostMan("test")
# 初始化PostMan测试数据
>>>BlueTest.initPostMan("test") #test.json.postman_collection->test.csv
>>> - INFO: postman转csv成功:./srcdata/test.csv
#依赖test.csv执行接口测试
>>>BlueTest.testByCsvData("test")
>>> - DEBUG: CSV文件内容序列化成功:[{'Lv': '', 'Cname': '', ...
>>> - INFO: log exceptionCheck: 普通请求 ...
>>> - INFO: log exceptionCheck: ['date']为空 ...
>>> - INFO: log exceptionCheck: ['date']不传 ...
>>> - INFO: log extrasCheck: ['date'] 额外参数校验 ...
接口压力测试Demo1
import BlueTest,random
class pressTest(BlueTest.SoloPress): #继承压力测试基类BlueTest.SoloPress
def setup(self):
self.count = 单线程执行数
def runcase(self): #重写runcase方法
response = random.choice(["成功","失败"]) #设置模拟测试数据
self.file_write(self.name, response, BlueTest.toolbox.responseAssert(response)) #结果记录
press = BlueTest.Press(线程数)
press.run(pressTest) #执行测试
press.dataReduction() #统计、整理测试结果
>>> index:1, run:10% ,num:2
>>> index:2, run:10% ,num:2
>>> ...
接口压力测试Demo2
区别于Demo1的地方在于这个例子使用到了由 postman->csv
的文件。将压力测试与接口测试的数据耦合到一起,可以实现统一管理。
csv2dict = BlueTest.Csv2Dict(path="./srcdata/test.csv") #加载测试数据 需要填入完整相对/绝对路径
csv_data = csv2dict.run() #生成测试数据
apitest = BlueTest.apiTest(csv_data[0]) #实例化测试单体
class solopress(BlueTest.SoloPress):
def setup(self):
self.count = 单线程执行数
def runcase(self):
response = apitest.soloRequest()
self.file_write(str(self.num),response,apitest.responseAssert(response))
press= BlueTest.Press(线程数)
press.run(solopress)
press.dataReduction()
PS:更多使用方法详见函数说明
DetailedSteps¶
在这里介绍如何使用 bluetest 一步一步完成工作。
首先让我们看下 bluetest 里的主要 .py
文件
logInit.py
日志相关parm.py
配置参数press.py
压测相关toolbox.py
工具箱dome_test.py
范例core.py
接口基础测试相关YApi2Csv.py
YAPI 一键转换为csv
准备数据¶
我们从第一步开始,准备数据。
按照我们的约定,做接口测试的源数据可以是 postman
里download下来的数据。如下图所示:

这样我们就获得了最原始的数据 test.json.postman_collection
名字很长,这是postman规定的,请暂时忍受一下。
由于这个文件的可读性比较差,而且不利于维护和使用,所以需要生成一个中间文件,在这里选用的是 csv
格式。
csv
作为 BlueTest 的标准数据文件,为了支持这一实现方式。
还提供了 postman
文件, 标准 swagger
,标准 YAPI
一键转换为 csv
的功能
而且不用担心,如果你的数据源不是以上格式,在后面的详细介绍中,会有 ``csv``文件格式的详细说明。可根据说明,编写自己的测试数据一键转换功能。
BlueTest.initPostMan(name,result_path = "")
function initPostMan
我们的一贯命名方式就是,中国式英语 - 命名风格。
顾名思义,这个函数的功能是初始化 postman
的数据。
调用方式可以有以下几种
BlueTest.initPostMan(name = "文件名缩写") #test.json.postman_collection 则只需要 initPostMan("test")
BlueTest.initPostMan(name = "文件完整路径") #完整的文件相对路径/绝对路径
BlueTest.initPostMan(name = "文件名缩写",result_path = "转换后文件路径")
result_path 不建议填写,不填写,会直接在 ./srcdata/
文件生成与数据源同名的文件,例test.json.postman_collection-> test.csv
initPostMan是如何工作的呢?
它的主要工作其实其实是进行文件名处理。除此以外的大部分功能其实是由我们一个类 Class Postman2Csv 来处理的。
首先initPostMan会将不确定的文件名(缩写,完整路径,绝对路径等) 进行处理后,变为标准格式后。将工作交于 Class Postman2Csv 。
postman2csv = BlueTest.Postman2Csv(path,result_path="")
postman2csv.run()
Class Postman2Csv
PostMan文件转换为Csv所使用的类。除了一大堆为了处理各种各样奇怪情况的代码以外。主要的进行工作的函数有:
data = postman2csv.getData(Cookie=False)
在这里有个常见的单词 Cookie ,由于PostMan导出的文件时常会带有一大堆很不重要的Cookie内容。为了降低后期的结果整理压力。在这里,我们默认关闭了Cookie选项。毕竟的用户鉴权,很多都不在Cookie里了,不论是单独的 Token,Sign 键值,还是Session方式。都更加流行。
当然,如果你还需要这个功能,可手动开启。
回到原来的话题,这个函数的功能是,用来获取 .postman_collection
文件的内容。并输出标准一个漂亮的数组,数组的内容是一堆长得不是很讨人喜欢的字典键值。
这样,我们就对 .postman_collection
文件进行了漂亮的序列化。这些数据的形状就任我们揉捏了。揉捏好的数据,按照约定,我们将它们变成 csv
格式。这就使用到了下一个函数。
postman2csv.write2Csv(data)
write2Csv = = write to csv 。如果你还看不明白含义,那么不是你的英文太好。就是中文不太好。 写入文件为csv格式
除了枯燥的将之前我们生成的漂亮数组 data
一条一条写入文件外。还增加了一些标志。用来加强可读性。

从上图可以看出,除了一行比较啰嗦的title以外,主要的标志有: START
, END
这两个标志位是用来规定每个测试用例的范围。
在这两个标志位以内就是一个测试用例,在这两个标志位以外的区域可以任由大家进行备注,而不影响测试用例。也算是在可读性和易读性之间的一种平衡。以上的工作搞定之后,如果你幸运的没有出现异常,那么测试数据的准备工作已经全部完成了
function initYApi2Csv
标准 YAPI
一键转换为 csv
BlueTest.initYApi2Csv(projects,csvname,apipath,api_user,api_pwd,project_url,login_path,user,pwd,tmp) # projects 需要生成csv文件的项目id # csvname 写入的csv文件名称 # apipath Yapi的域名 # api_user Yapi登录用户名 # api_pwd Yapi登录密码 # project_url 项目域名 # login_path 项目登录path # user 项目登录用户名 # pwd 项目登录密码 # tmp 可能会缺少的path
生成的csv文件在``./srcdata``目录下,内容同BlueTest.Postman2Csv生成文件一致。
接口测试¶
由于每个人,每个部门,每个公司的业务需求千奇百怪,所以,作为一个通用性的库。故我们暂时不考虑这些特性的东西。先把共性的问题解决。比如 值为空
键值均为空
额外参数校验
参数长度校验
bluetest 对以上功能均做到了一键式的执行。改点配置,执行,你就能获得一堆漂亮(啰嗦)的测试数据。使用者的工作,就从一个一个机械化的填异常参数,变成了只要点一下。
如何进行测试,如何配置这些东西呢?我们一步一步来。
- 首先确保数据准备好了,有一个生成的csv文件
- 然后执行以下语句
BlueTest.testByCsvData(name,normal_test=True,mkpy=False,limit_check = False,extras_check=True,encode="",case_type="",counter=True,need=0) # csv 名称(test.csv->testByCsvData("test") ) or 绝对路径/相对路径 (testByCsvData("./tmp/test.csv") ) # normal_test 基础测试 # mkpy实时生成单接口.py文件 # limit_check 参数长度校验 # extras_check额外参数校验 # case_type="HeartTest" 只进行普通请求校验 # need=n 从第n个需要执行的用例开始执行
- 执行结束之后。恭喜你,做完了。去
./result/data.txt
里看结果吧。 - 想要自动分析执行结果,查看``./result/result_error.txt``,如执行结果内不包含”0x000000”,会写入到该文件,包含接口请求地址、返回code、http响应code、message信息、type错误类型等。
type错误类型
- 错误信息不明确:返回数据为空
- 服务不存在:http_code返回500
- 缺少参数或参数不正确:参数问题
- 业务性问题:由于业务特殊性造成,可忽略
- 其他:404,网络异常,PHPerror,未知错误,接口异常等
收起你一脸蒙蔽的表情,没错。做完了。整理数据,去发测试报告吧!但是如何执行的,你肯定很好奇。我们再次一步一步来。
function testByCsvData
依据 csv
数据进行测试,一堆入参的含义就不赘述了。它的主要工作其实和 initPostMan 很相似。主要做的是入参标准化。之后实例化了 Class Csv2Dict
,class Dict2Py
和 class ApiTest
Class Csv2Dict
这是又一次做序列化的工作了。这次是把标准的中间文件 csv
处理为使用起来最舒服的字典类型。
>>>import BlueTest >>>csv2dict = BlueTest.Csv2Dict("./srcdata/test.csv").run() >>>for key,value in csv2dict[0].items(): >>> print (key,":",value) - DEBUG: CSV文件内容utf8序列化失败重试:'utf-8' codec can't decode ... - DEBUG: CSV文件内容序列化成功:[{'Lv': ''.... Lv : Cname : Name : log Describe : testapi ResualPath : ./api/log Method : POST Url : https://www.test.com/action/api/log Headers : {'Origin': 'https://www.TEST.net', 'User... Data : {"date":"2018-11-04 10:21:06","action... DataType : raw UrlParams : {'requestID': 'Abac6b... TestType :
以上是一个完整的测试数据,部分空的内容,是预留给你使用的,当然,理想的情况是不使用。那就说明, bluetest 已经满足你的需求了。
Class Dict2Py
大家在使用 postman
的时候,应该玩过 Generate Code
这个漂亮的功能,可以将内容一键转为各种你需要的语言代码。一个很好,很实用的功能。所以,我们很谦虚(无耻)的兼容(抄袭)了这个功能。这个类就是做这件事情的。
>>>import BlueTest >>>csv2dict = BlueTest.Csv2Dict("./srcdata/test.csv").run() >>>BlueTest.dict2Py(data = csv2dict[0]).mkpy()
执行后,我们获得了 ./result/api/log.py
可以发现地址和上面的某一条 ↓↓
ResualPath : ./api/log
↑↑ 一致。所以大家不用担心,自己的生成的文件会被推在一起,造成困扰。
Class ApiTest
接口测试的基类
>>>import BlueTest >>>apitest = BlueTest.ApiTest(data)
还是一样,从csv里抽一条数据作为入参,来实例化基类。在基类的构造函数来里可以看到,里面对接口的一些测试标准进行了配置。而且一切的初始化都是基于 param.py
,详细的配置内容,请自行查看相关文件。
在实例化获得 apitest
后,众所周知,构造函数已经运行了。这个时候。我们的数据准备已经完成。
执行具体的接口校验工作的方法有 limitCheck
(长度校验) exceptionCheck
(空校验) extrasCheck
(额外参数校验)。
校验的执行方式如下:
>>>csv2dict = BlueTest.Csv2Dict("./srcdata/test.csv").run() >>>apitest.dataReduction(csv2dict[0]) #正常用法 >>>apitest.dataReduction(csv2dict[0],limitcheck=True,extras_check=True) #进行部分校验的用法
至此。接口测试的活干完了。之后就是结果分析和统计了。相关内容将在结果分析相关章节里进行叙述。 为了便于大家使用。接口基础测试的完整代码如下:
>>>#确保该路径下,存在该文件 ./srcdata/test.json.postman_collection >>>BlueTest.initPostMan("test") #数据准备 >>>BlueTest.testByCsvData("test") #测试执行
压力测试¶
关于接口,除了日常的接口测试外,还有时常遇到的压力测试。由于现在更多的服务器在云端,而且各个云服务提供商,都有非常好的系统/应用监控系统。故,我们暂时跳过了服务器的监听。这些,大家只要根据时间戳,找运维拉数据就行。大家也不用痛苦的在服务器上安装JmeterPlugins之类的工具来监听了。毕竟专业的事情交给专业的人,这样才能提高效率和更好的做好自己本职的工作。世上从来不存在高大全的系统。也不存在完美的人。
还是先来几个例子:
Demo1
>>>import BlueTest,random >>>class PressTest(BlueTest.SoloPress): def runcase(self): response = random.choice(["成功","失败"]) self.file_write(str(self.num), response, BlueTest.toolbox.responseAssert(response)) press= BlueTest.Press(2) #线程数 press.run(PressTest) press.dataReduction()
Demo2
>>>import BlueTest >>> csv_data = BlueTest.Csv2Dict(path="./srcdata/test.csv").run() apitest = BlueTest.apiTest(csv_data[0]) class PressTest(BlueTest.SoloPress): def runcase(self): response = apitest.soloRequest() self.file_write(str(self.num),response,b.responseAssert(response)) press= BlueTest.Press(2) #线程数 press.run(PressTest) press.dataReduction()
Demo3
>>>import BlueTest >>> temp = ["id1", "id2", "id3"] #① apitest = BlueTest.apiTest(csv_data[0]) class PressTest(BlueTest.SoloPress): def setup(self): self.num = temp[self.index-1] #② def runcase(self): apitest.data[BlueTest.csv_parm.DATA]["ID"] = self.num #③ response = apitest.soloRequest() self.file_write(str(self.num),response,b.responseAssert(response)) press= BlueTest.Press(3) #线程数 press.run(PressTest) press.dataReduction()
如果大家耐得住性子的话,会看出。这三个例子明显的区别。 Demo1
使用的是随机生成的假数据。 Demo2
使用的是csv里的第一个接口的数据 Demo3
在 Demo2
的基础上,增加了一些自定义参数。
从实际使用的角度而言, Demo3
是我们再实际工作中最常使用到的。除了大部分的模板式代码以外。其实需要手动编写的主要部分是自定义数据的部分。
全部的代码大概3行。 ① 数据初始化 ,② 线程获初始化据数, ③ 执行前,赋值。
Demo大家看到了。除此以外, BlueTest 里,也自带了两个相关的demo
>>>import BlueTest >>>BlueTest.presstest() >>>BlueTest.pressTestByCsv() end
presstest()
执行肯定不会出现问题的,因为数据是我们随机生成的。但是 pressTestByCsv()
如果出问题的话…..放心,不会是大问题,耐心点
Demo说完,我们开始一步一步介绍,到底是如何工作的
Class Press
大家可以理解为,这是一个多线程的盒子,它自动生成多线程(我们最讨厌的东西)。实例化这个类的时候。就直接确定了,产生的线程数
>>>import BlueTest
>>>BlueTest.Press(线程数)
除此之外,压力测试最重要的一点就是对执行数据的整理,因为这才是我们需要的。这才是最后测试报告里需要体现的内容。为此我们写了一个方法 dataReduction
function dataReduction
这是 Class Press
中用来进行数据整理的方法。入参默认不填,或者填入你的压测结果路径 Press_press.log
。
数据是基于三个维度进行的整理:
- 自然时间流失过程中,接口请求的效率
- 所有请求的耗时
- 请求的成功率
- 为了便于统计和整理。我们将原始数据里的毫秒级数据整合成了秒级的数据(请求耗时除外)。并且输出位表格格式
time.csv
,resualt.csv
time.csv
可以看到,左侧是根据请求耗时(毫秒)对请求进行的统计。当然,图中右边的图表需要大家使用excel手动生成。
resualt.csv
![]()
时间轴变成了秒,并增加了成功与失败的统计。
在不进行调优的情况下,作为客户端可以看到的大部分内容,都已经包含在 resualt
和 time
中。服务端的数据,大家可以根据 resualt.csv
中的时间戳,从监控系统中获取相关服务端状态和日志。当然,建议大家还是骚扰相关管理员或者运维来获取相关内容。
function run
作为 Class Press
中的主执行函数,它的入参比较特别,是一个类 Class SoloPress
。具体这个类是做什么我们稍后再讨论,先假设,我们已经有这么一个类了。让我们看一下 function run
究竟干什么了。
>>>def run(self,solopress): ThreadList = [] lock = threading.Lock() for i in range(1, self.num+1): t = solopress(lock,i) t.setup() ThreadList.append(t) for t in ThreadList: self.runSleep() t.start() for t in ThreadList: t.join()
代码很短,而且很常见,简单来说。就是根据线程数 self.num
来新建线程,并运行他们。关于 threading.Lock()
的相关内容就不再这里说了。毕竟,锁是一个很复杂的东西。
简单的总结一下 Class Press
就有一个用来新建多线程,执行。并最后进行数据整理的类。
下面介绍下,出现了好几次的 Class SoloPress
。
Class SoloPress
从所继承的父类可以看出来 Class Press
继承的是object。 Class SoloPress
继承的是 threading.Thread。由此也可以看到,SoloPress与多线程相关,所以继承了线程控制类。按照正常的使用方法。我们需要重写部分函数函数:数据准备函数 function setup
,单次执行函数 funciton runCase
。这里要介绍的不是这些注定要被大家重写的函数。如果想了解用法请看上面的DEMO。而是主要介绍一下 Class SoloPress
本身为用户做了什么事。
function run
按照正常情况,run函数是类实例化后的主执行函数。所以在这里。我们做了单线程的接口调用。除了执行规定次数的 runCase
外,还进行了一些其他辅助类的工作,比如:线程执行的进度展示,执行数据的记录… 听上去是一些无关紧要的东西。但是却能提高很多用户体验,毕竟,谁也不想执行代码后,只能经过一段没有进度的等待,获得一堆没有规划好的数据。
异步压力测试¶
我们平时使用到的脚本思想,更多的还是同步的做法。例如`requests` 。我们请求一个接口的时候,需要发送请求,并且等待结果。在发送请求到接收请求的这段时间内,程序是被阻塞的。而且阻塞的成本根据情况而言,有可能会很大。
在做压力测试的时候,过多的线程数,会极度的消耗客户端的资源。使我们没有办法给服务器足够的压力。
- 为了解决这个问题,我们使用了异步的方式来进行了压力测试。
- asyncio 一个容易让人不想看下去的库。
- aiohttp 由于上手难度最低的`requests`并不支持异步的方式。所以用到这个库。
Demo¶
首先让我们看下Demo
class Test(BlueTest.SoloPressAsync):
async def setup(self, **kwargs):
kwargs["data"]["code"] = await self.queue.get()
return kwargs
def newQueue(self):
temp_list = []
for i in range(0, 1000000):
if len(str(i)) < 6:
i = "0" * (6 - len(str(i))) + str(i)
temp_list.append(str(i))
queue = asyncio.Queue()
[queue.put_nowait(temp) for temp in temp_list]
self.queue = queue
if __name__ == '__main__':
new = Test("http://hq.sinajs.cn/list=sh600001", "Get", headers={"requestTime": "test",
"User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50 IE 9.0"},
vuser=500, total_num=2000,
data={"code": "", "phone": "13111111111"})
new.mainrun()
new.dataReduction()
在 if __name__ 之上是我们的测试类。可以看到我们继承了 BlueTest.SoloPressAsync 。然后重写了两个函数 setup , newQueue。
setup
数据初始化newQueue
建立数据队列
大家可以看到,以上的两个函数,都是为了压力时的数据定制化而存在。基本思想是,在 newQueue 里进行数据组装和入队。在 setup 里进行数据赋值与出队。如果不需要定制化数据,以上操作均不需要。
详细说明¶
Class SoloPressAsync
构造函数
def __init__(self,url,method,path=”./result/Press_press.log”,vuser=5,total_num=10,**kwargs):
由此可以看出,初始化它或者它的子类的时候。我们需要传入不少参数。
url
请求地址method
“GET” or “POST” or “OPTION” ….path
结果文件,一般不需要更改vuser
虚拟用户数,在不更改系统配置的情况下最大值为500total_num
总请求数**kwargs
用来传入http请求的其他参数,比如,data,params,headers… 与 requests 规则一致
所以一般的实例化方式如下:
- new = Test(“请求地址”, “Get”,vuser=500, total_num=2000,
- data={“code”: “”, “phone”: “”}, headers={“User-Agent”: “Test”})
暂时只做了测试执行前的,自定义数据。暂时没做多接口的场景模拟demo。 以下说明两个可能需要用户重写的函数。
newQueue
1 def newQueue(self):
2 temp_list = []
3 for i in range(0, 1000000):
4 if len(str(i)) < 6:
5 i = "0" * (6 - len(str(i))) + str(i)
6 temp_list.append(str(i))
7 queue = asyncio.Queue()
8 [queue.put_nowait(temp) for temp in temp_list]
9 self.queue = queue
我们来简单解释一下以上代码。2-6行,我们生成了一个自定义的数组。内容并没有特定的含义,大家可以根据实际的业务需要来新建自己的数据。 第7行,新建了一个asyncio的队列。 第8行,数组的值写入队列 第9行,赋值 self.queue
至此,我们再测试中需要使用到的数据已经搞定了。
setup
async def setup(self, **kwargs):
kwargs["data"]["code"] = await self.queue.get()
return kwargs
通过之前的 newQueue
组装好数据之后。我们需要在测试过程中使用到它。我们需要重写 setup
函数。首先这个函数需要添加异步的标签。并且入参是 **kwargs 是为了方便后面的基础函数直接使用数据进行异步请求。
kwargs["data"]["code"] = await self.queue.get()
相当于
new = Test(“请求地址”, “Get”,vuser=500, total_num=2000,
data={“code”:await self.queue.get()
, “phone”: “”}, headers={“User-Agent”: “Test”})
当然,实际使用肯定不是这样。这行代码只是为了便于读者理解数据是传入了哪里。
执行 在大概理解以上内容后。我们就可以使用自己的自定义函数来执行异步的压力测试了。
new.mainrun() #执行
new.dataReduction() #数据整理
这里的执行原始数据与press类的结果格式完全相同。数据整理结果也完全相同。
更多的场景DEMO,还没写
Demos¶
每一个测试工程师都是一位挑剔的处女座。 对于我们非常不漂亮的代码,如果你看完还没有想吐的话,一定不是一个合格的测试工程师。 为了避免被大家吐槽,为了降低大家看我们源码的恶心程度。在这里。我们讲写出各种使用DEMO。
Demo1:test¶
Markup¶
The following are examples of supported markup. On their own, these will not provide a datepicker widget; you will need to instantiate the datepicker on the markup.