本文是《手把手教你用Python实现接口自动化测试》这一系列文章的开篇,笔者将从本文开始给大家介绍一下如何使用Python实现接口自动化测试,希望能起到一个抛砖引玉的作用。
在《手把手教你用Python实现接口自动化测试》这一系列文章将会分成3个部分:
1. 开篇:介绍Python实现接口自动化的实现思路,项目结构以及展示最终实现的效果;
2. 支线文章:针对项目中所用到的知识点分别进行展开介绍;
3. 完结篇:我将在最后一篇文章中对项目中各个细节进行详细阐述。
前言
开篇文章中我们先主要来说一说在开始写实际业务代码之前的一些准备工作,介绍一下使用Python实现接口自动化的实现思路。在这个系列文章里不会教您Python
的基础知识,如果您知道一些简单的编码知识或者进行一定的Python基础学习后会有助于您对本系列文章的阅读。
一、为什么需要实现接口自动化测试?
首先,我们先看一下正常的接口测试流程是什么?
这其中可以实现接口测试的工具有很多,例如:如postman、jmeter、fiddler等等,这些工具使用起来非常方便,功能也很多。
那么为什么还要写代码实现接口自动化呢?
-
工具虽然方便,但也其不足之处:
1. 测试数据不可控制
接口测试本质其实是对数据的验证:我们调用接口,输入接口请求需要的一些数据,接口返回一些数据,然后我们针对接口返回数据验证正确性。在用工具运行测试用例之前不得不手动向数据库中插入测试数据。这样我们的接口测试是不是就没有那么“自动化了”。
2. 回归测试耗时
这是功能测试的一大硬伤,测试人员往往无法面对成百上千条测试case的回归需求,而大部分开发在完成一个改动时都会捎带一句“全量回归一下,看看有没有其他问题”。其次即使我们的测试人员比较给力认真,完成了回归测试,但这其中难免不会有失误或者遗漏之处。机器却能任劳任怨的执行每一条case且保证每次都能按照流程进行测试。
3. 扩展能力不足
当我们在享受工具所带来的便利的同时,往往也会受制于工具所带来的局限。例如,我想将测试结果生 成 HMTL 格式测试报告,我还想将测试报告发送到指定邮箱等等,这些需求都是工具难以实现的。
那么我们期望的接口自动化测试流程是什么样的?
本系列文章中介绍的接口自动化测试框架还能做什么?
1:测试数据源可以是多样性的
2:多种结果验证方式
当然我们还可以将测试结果处理为HTML格式的报告以邮件的形式发送至指定邮箱。
那么接下来,我们就根据这样的过程来一步步搭建我们的框架。在这个过程中,我们需要做到业务和数据的分离,这样才能灵活,达到我们实现接口自动化目的。
二、测试需求梳理
无论是正常测试还是实现接口测试自动化,我们都应该优先将需求梳理清晰,这样才能在执行的过程中确保不会有偏差。
本文将以QQ音乐PC版中歌手列表和排行榜相关功能做示例,示例截图如下:
QQ音乐PC版首页传送门
QQ音乐PC版-歌手首页
通过简单的页面分析,我们可以获取歌手列表相关接口信息:
1. 歌手列表首页
1.1 歌手列表首页接口请求信息
通过cur_page字段的值控制请求页数,一页返回80条
第一页
https://u.y.qq.com/cgi-bin/musicu.fcg?-=getUCGI1473476396452713&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={"comm":{"ct":24,"cv":0},"singerList":{"module":"Music.SingerListServer","method":"get_singer_list","param":{"area":-100,"sex":-100,"genre":-100,"index":-100,"sin":0,
"cur_page":1
}}}第二页
https://u.y.qq.com/cgi-bin/musicu.fcg?-=getUCGI9803531398820899&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={"comm":{"ct":24,"cv":0},"singerList":{"module":"Music.SingerListServer","method":"get_singer_list","param":{"area":-100,"sex":-100,"genre":-100,"index":-100,"sin":80,
"cur_page":2
}}}
1.2 歌手列表首页接口返回信息
2. 歌手热门歌曲列表
2.1 周杰伦-热门歌曲列表接口请求信息
通过singermid字段的值控制请求的歌手歌曲列表
通过sin,num字段的值控制请求列表数量,默认返回10条
https://u.y.qq.com/cgi-bin/musicu.fcg?-=getUCGI3347917939773577&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={"comm":{"ct":24,"cv":0},"singer":{"method":"get_singer_detail_info","param":{"sort":5,
"singermid":"0025NhlN2yWrP4"
,"sin":0
,"num":10
},"module":"music.web_singer_info_svr"}}
2.2 周杰伦-热门歌曲列表接口返回信息
3. 歌手专辑列表
3.1 周杰伦-专辑列表接口请求信息
通过singermid字段的值控制请求的歌手歌曲列表
通过begin,num字段的值控制请求列表数量,默认返回5条
https://u.y.qq.com/cgi-bin/musicu.fcg?-=getUCGI3111122848094381&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={"comm":{"ct":24,"cv":0},"singerAlbum":{"method":"get_singer_album","param":{
"singermid":"0025NhlN2yWrP4"
,"order":"time","begin":0
,"num":5
,"exstatus":1},"module":"music.web_singer_info_svr"}}
3.2 周杰伦-专辑列表接口返回信息
QQ音乐PC版-排行榜首页
让我们再通过简单的页面分析,我们可以获取排行榜相关接口信息:
4. 排行榜
4.1 流行指数榜-歌曲列表接口请求信息
通过topId字段的值控制请求的榜单列表
通过offset,num字段的值控制请求列表数量,默认返回20条
https://u.y.qq.com/cgi-bin/musicu.fcg?-=getUCGI2506723965565383&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={"detail":{"module":"musicToplist.ToplistInfoServer","method":"GetDetail","param":{
"topId":4
,"offset":0
,"num":20
,"period":"2019-08-14"}},"comm":{"ct":24,"cv":0}}
4.2 流行指数榜-歌曲列表接口请求信息
最后,通过以上简单的分析,我们设定有如下测试需求:
三、项目搭建
我们把搭建一个项目比喻成建造一间房屋,其中我们需要先为房屋建立基础和框架(项目结构)就像是先建成一座毛坯房。
我们在建造房屋时所使用到的工具:
-
编程语言:Python 3.7
-
编译器: Pycharm
-
项目框架:Flask
-
测试框架:Unittest
使用Pycharm创建Flask项目后,我们建立如下项目结构:
********项目中各个文件作用********
common
------存放公共的方法文件
config
------存放配置文件
testcase
---存放具体的测试case
testdata
---存放相关测试数据
testresult
---存放测试报告和日志文件
testrunner
---用例执行的入口文件
util
----------私有工具文件
app.py
-------接口入口文件
现在我们有了一间光秃秃的毛坯房,里面什么都没有,那么接下来我们需要对其进行装修使其拥有较为舒适的居住条件。
1. 首先我们从比较容易的config
文件入手
config目录下有2个.ini
信息配置文件,以及2个用于读取配置文件信息的.py
文件。
1.1 配置文件信息
commoninfo.ini
文件配置信息如下(配置了各个api接口的请求参数信息)
[UrlInfo_QQMusic]---接口请求Url信息分组标识
apibaseurl = https://u.y.qq.com/cgi-bin/musicu.fcg?-=getUCGI1473476396452713&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data=
data_singerlist = {"comm":{"ct":24,"cv":0},"singerList":{"module":"Music.SingerListServer","method":"get_singer_list","param":{"area":-100,"sex":-100,"genre":-100,"index":-100,"sin":0,"cur_page":1}}}
data_songlist = {"comm":{"ct":24,"cv":0},"singer":{"method":"get_singer_detail_info","param":{"sort":5,"singermid":"0025NhlN2yWrP4","sin":0,"num":10},"module":"music.web_singer_info_svr"}}
data_albumlist = {"comm":{"ct":24,"cv":0},"singerAlbum":{"method":"get_singer_album","param":{"singermid":"0025NhlN2yWrP4","order":"time","begin":0,"num":5,"exstatus":1},"module":"music.web_singer_info_svr"}}
data_weeklist = {"comm":{"ct":24,"cv":0},"request":{"method":"get_history_key_list","param":{"periods":5},"module":"video.VideoRankServer"}}
data_toplist = {"detail":{"module":"musicToplist.ToplistInfoServer","method":"GetDetail","param":{"topId":4,"offset":0,"num":20,"period":"2019-08-14"}},"comm":{"ct":24,"cv":0}}
dbconfig.ini
文件配置信息如下(配置了数据库连接参数)
[DataBase_ConnectInfo]---数据库连接参数分组标识
host = **********你需要操作的数据库DB连接串
port = **********DB端口号
dbname = ********DB名称
username = ******DB连接账号
password = ******DB连接密码
1.2 读取配置文件
读取配置文件信息思路:
使用configparser
对象在对应配置文件中根据分组标识获取相关配置参数
以readdbconfig.py
为例,其读取dbconfig.ini
文件信息的代码如下:
currentfile_path = os.path.split(os.path.realpath(__file__))[0]
"""拼接config.ini文件的路径地址"""
dbconfig_path = os.path.join(currentfile_path, "dbconfig.ini")
"""实例化configParser对象"""
self.cf = configparser.ConfigParser()
"""读取config.ini文件"""
self.cf.read(dbconfig_path, encoding="utf-8")
"""根据配置文件中的分组标识获取配置参数信息"""
connectinfo = dict(self.cf.items('DataBase_ConnectInfo'))
2. 下面我们看看common
目录下面都有哪些公共方法
2.1 excutesql.py
文件用于数据库中进行sql语句的执行操作
该封装方法进行了数据库的连接操作,外部调用只需传入需要执行的sql语句即可
2.2 getlogger.py
文件用于创建logger对象
该封装方法创建了一个
logger
对象,供所有case执行时进行相关信息日志的记录
2.3 gettestdata.py
文件用于获取case执行时的测试数据
该封装方法是我们实现数据分离的核心
主要通过获取
api接口
/mysql
/excel文件
/config配置文件
中的数据,然后将数据转成我们case执行时需要的数据格式
当前项目中我每个接口分别使用了不同的数据源获取方式进行演示,在实际项目中可根据情况选择相应的数据源获取方式。
2.4 MyHTMLTestRunner.py
文件用于将测试结果生成*.html
格式的测试报告
该文件为网上其他大神的作品,我进行了些许的改动,就不提供源码了,大家可自行google即可
2.5 sendemail.py
文件用于将测试结果邮件的形式发送至指定邮箱地址
该封装方法是将我们测试结果(包含
*.html
和*.log
附件)通过邮件的形式发送至指定接收人
其中邮件收件人:recipients
、邮件抄送人:cc
信息,我这边是通过在上文commoninfo.ini
文件中进行配置,使用configparser
对象进行读取
#commoninfo.ini中信息---多个收件人、抄送人以分号隔开
[EMAIL_OUTLOOK]------邮件配置参数信息分组标识
recipients = ***1@***.***; ***2@***.***;
cc = ***1@***.***; ***2@***.***;
3. 我们现在看看测试数据testdata
目录有哪些内容
testdata
目录下一般存放的是一些静态的 测试数据文件;文件类型不固定,如:.txt
、.xls
、.xml
、.json
都能支持;
思路就是通过各个文件的类库方法从文件中读写数据后,通过gettestdata.py
封装成我们最终需要的测试数据格式。
4. util
中放的是一些私有工具类
util
中的工具类一般都是针对当前项目封装的一些测试工具类,对于别的项目可能就不太适用。
本文中笔者针对excel
文件进行读写封装了一个简单工具类operateexcel.py
5. 实际的测试case文件
上面我们有看到测试数据参数的设置,测试数据的获取方式等等,但这些都是前置条件,而实际的测试case是什么样的呢?
我们将测试用例case维护在testcase
文件目录下
首先我这边根据测试接口类型进行了分类:排行榜相关接口、歌手相关接口;
然后每个接口单独一个
.py
文件进行具体测试case的编写。
本文编写case时采用的是
python
自带的unittest
测试框架对case进行驱动执行。
每个case文件在被执行之前需求进行测试数据的初始化;
测试数据通过parameterized
进行参数化传参;
使用assertEqual
进行断言验证。
以
singerlist.py
为例:该文件为“验证每个歌手基本信息中各个字段的信息”测试case
@parameterized.expand(testdata['singerinfo_list'])
def test_Country_Null(self, singerinfo):
"""
验证每个歌手信息中的country不为空
:param singerinfo:
:return:
"""
country = singerinfo['country']
if country:
self.assertEqual(True, True)
else:
print(singerinfo)
self.logger.info('singerinfo_test_Country_Null:' + str(singerinfo))
self.assertEqual(True, False)
@parameterized.expand(testdata['singerinfo_list'])
def test_Singer_Id_Type(self, singerinfo):
"""
验证每个歌手信息中的singer_id数据类型为数字
:param singerinfo:
:return:
"""
singer_id = singerinfo['singer_id']
if singer_id.__class__.__name__ == 'int':
self.assertEqual(True, True)
else:
print(singerinfo)
self.logger.info('singerinfo_test_Singer_Id_Type:' + str(singerinfo))
self.assertEqual(True, False)
@parameterized.expand(testdata['singerinfo_list'])
def test_Singer_Mid_Len(self, singerinfo):
"""
验证每个歌手信息中的singer_mid数据长度
:param singerinfo:
:return:
"""
singer_mid = singerinfo['singer_mid']
if len(singer_mid) == 14:
self.assertEqual(True, True)
else:
print(singerinfo)
self.logger.info('singerinfo_test_Singer_Mid_Len:' + str(singerinfo))
self.assertEqual(True, False)
@parameterized.expand(testdata['singerinfo_list'])
def test_Singer_Name_Type(self, singerinfo):
"""
验证每个歌手信息中的singer_name数据类型
:param singerinfo:
:return:
"""
singer_name = singerinfo['singer_name']
if singer_name.__class__.__name__ == 'str':
self.assertEqual(True, True)
else:
print(singerinfo)
self.logger.info('singerinfo_test_Singer_Name_Type:' + str(singerinfo))
self.assertEqual(True, False)
@parameterized.expand(testdata['singerinfo_list'])
def test_Singer_Pic_Info(self, singerinfo):
"""
验证每个歌手信息中的singer_pic数据内容
:param singerinfo:
:return:
"""
singer_pic = singerinfo['singer_pic']
singer_mid = singerinfo['singer_mid']
exceptvalue = 'http://y.gtimg.cn/music/photo_new/T001R150x150M000' + singer_mid + '.webp'
if singer_pic == exceptvalue:
self.logger.info('singerinfo_test_Singer_Pic_Info:' + str(singerinfo))
self.assertEqual(True, True)
else:
print(singerinfo)
self.assertEqual(True, False)
四、测试case的执行
1. 如何执行维护的测试case
经过前面的操作,我们现在有了一间装修完的房子,里面有家具家电,现在我们要做的就是给它上通上水电煤,让它能运转起来满足我们持续居住的条件。同样,我们的测试case也需要被执行才能实现其存在的意义,下面我们来看看我们的测试case如何被驱动执行的。
我们将测试用例执行文件维护在testrunner
文件目录下
我们可以在
caserunner_myhtml.py
文件中定制我们的case执行方式:
- 执行所有接口的测试case
- 根据接口名称执行测试case
def runall(self):
"""1.根据当前文件路径获得case执行时的路径、报告名称信息"""
pathinfo_dict = TestData().get_pathinfo_dict(__file__)
"""2.初始化测试套件"""
testsuite = unittest.TestSuite()
"""3.遍历appidlist中的应用appid,获取对应应用下所有接口case,并添加进测试套件testsuite中"""
global discover, case_list
case_list = [] # 当前appid下对应的测试用例列表
"""3-1.拼接当前appid的用例路径"""
filepath_testcase = pathinfo_dict['filepath_testcase']
"""3-2.使用os.walk方法遍历得到所有文件名称filename的列表集合"""
for dirpath, dirname, filename in os.walk(filepath_testcase):
for file in filename:
# 判断文件以.py结尾且不以__开始,为去除__init.py文件和.pyx后缀的文件
if file.endswith(".py") and not file.startswith("__"):
# print(file)
case_list.append(file)
"""3-3.使用unittest.defaultTestLoader.discover()方法,根据路径和case名称寻找对应的case信息"""
for case in case_list:
discover = unittest.defaultTestLoader.discover(start_dir=filepath_testcase,
pattern=case,
top_level_dir=filepath_testcase)
"""3-4.将找到的case添加至测试套件中"""
testsuite.addTest(discover)
"""4.执行所有测试套件testsuite中所有测试用例,生成报告,并将报告发送至指定邮箱地址"""
self.run_and_sendemail(testsuite, pathinfo_dict['reportpath'])
def runall_bycaselist(self, caselist):
"""
通过根据传入的case名称列表进行所有case的执行
1.传入多个case名称即为执行列表中所有接口case
2.传入单个case名称即执行case下所有接口case
:param caselist:
:return:
"""
"""1.根据当前文件路径获得case执行时的路径、报告名称信息"""
pathinfo_dict = TestData().get_pathinfo_dict(__file__)
"""2.初始化测试套件"""
testsuite = unittest.TestSuite()
"""3.遍历caselist中的case名称,获取对应接口case,并添加进testsuite"""
for case in caselist:
global discover
"""
3-1.使用unittest.defaultTestLoader.discover()方法,根据路径和case名称寻找对应的case信息;
filepathdict['filepath_testcase']对应为filepath_testcase
case对应的文件名称
"""
discover = unittest.defaultTestLoader.discover(start_dir=pathinfo_dict['filepath_testcase'],
pattern=case + '.py',
top_level_dir=None)
"""3-2.将找到的case添加至测试套件中"""
testsuite.addTest(discover)
"""4.执行所有测试套件testsuite中所有测试用例,生成报告,并将报告发送至指定邮箱地址"""
self.run_and_sendemail(testsuite, pathinfo_dict['reportpath'])
2. 生成测试报告
本文中,我们在
caserunner_myhtml.py
文件将用例的执行、测试报告的生成、以及邮件的发送进行了封装。
可在testresult
文件目录下查看测试报告和日志文件信息。
到目前为止我们其实已经实现了目标,但是以上操作都是在本地环境进行执行的,接下来我们还需要考虑如何让该工具被所有人使用,才能实现其最大的价值。
3. 使用接口进行调用
一般我们实现了一个测试工具,往往不能只考虑是提供给测试人员使用(或者说只是编写测试用例的同学才会使用);
我们希望开发人员或者其他相关人员都能使用,但是他们又无需在本地部署代码;
所以我们需要考虑如何将测试工具变得容易操作,比如只需调用接口就能执行相关用例;
这个就是我们为什么使用Flask进行项目搭建的原因:
我们使用Flask进行实现了简单的restful风格的接口服务,在内部调用我们的用例执行文件caserunner_myhtml.py
,从而达到接口调用的效果。
通常我们
Flask
项目的入口放在默认的app.py
文件中,这个文件名称不是固定的,可以进行自定义以便不同服务应用接口的区分。
@app.route('/runallcase', methods=['GET'])
def runallcase():
"""
执行所有测试用例接口
:return:
"""
"""调用CaseRunner中的runall()方法"""
CaseRunner().runall()
return 'Success'
@app.route('/runallcase_bycaselist', methods=['POST'])
def runallcase_bycaselist():
"""
根据传入的接口名称执行所有测试用例接口
:return:
"""
"""1. 获取接口请求post的请求报文信息,并处理为json格式"""
request_json = request.json
"""2. 获取请求参数中的caselist参数信息"""
caselist = request_json['caselist']
"""3. 调用CaseRunner中的runall_bycaselist()方法"""
CaseRunner().runall_bycaselist(caselist)
return 'Success'
postman请求效果
五、最终实现效果
通过以上一顿操作后,我们来看看最终实现的效果图
1. 测试报告邮件中的内容
邮件内容包括测试结果的预览信息,附件中报告了生成的
*.html
、*.log
文件
2. 生成的html
测试报告内容
附件中的
*.html
文件为整个测试用例执行后的结果信息,包含了测试人员、开始时间、执行耗时等信息
还可以通过点击详细按钮查看错误详情
3. 生成的log
日志文件内容
总结
以上就是我们使用Python
和 Flask
框架搭配Unittest实现的一个接口自动化测试框架,我将在接下来的章节中,针对该项目中涉及到的一些类库使用做相应的介绍。
占坑
支线文章
- 《使用Pycharm创建Flask项目》
- 《配置文件的读写》
- 待定
- 待定