接口测试 基于 python 的接口测试框架

adonisjph · May 20, 2016 · Last by adonisjph replied at January 14, 2019 · 15789 hits

项目背景
公司内部的软件采用B/S架构,管理实验室数据,实现数据的存储和分析统计。大部分是数据的增删改查,由于还在开发阶段,所以UI界面的变化非常快,之前尝试过用python+selenium进行UI自动化测试,后来发现今天刚写好的脚本第二天前端就改了页面,又得重新去定位元素什么的,消耗大量的精力与时间维护自动化脚本。针对此种情况,对接口测试较为有效。

工具
由于开发那里不能提供后台代码给我,只能通过抓包分析。使用fiddler抓包,发现前端与后台都是采用POST方法交互,前端POST数据,后台返回数据。这样也比较简单粗暴,直接针对每个接口POST测试数据,然后观察返回值就好了。

使用excel编写测试用例和数据,requests发送HTTP请求。

功能模块
通过xlrd库读取excel中的测试用例和数据
requests负责发送数据并接收后台返回的数据
针对测试结果,需要保存测试日志并生成测试报告,采用python自带的logging记录日志,测试报告采用html格式。
同时测试完成后,需要将测试报告发送给相关的开发人员,需要有自动发送邮件的功能

目录结构

代码实现

#通过自带的ConfigParser模块,读取邮件发送的配置文件,作为字典返回
import ConfigParser

def get_conf():
conf_file = ConfigParser.ConfigParser()

conf_file.read(os.path.join(os.getcwd(),'conf.ini'))

conf = {}

conf['sender'] = conf_file.get("email","sender")

conf['receiver'] = conf_file.get("email","receiver")

conf['smtpserver'] = conf_file.get("email","smtpserver")

conf['username'] = conf_file.get("email","username")

conf['password'] = conf_file.get("email","password")

return conf

配置文件格式
这个logging不熟悉的可以google一下,还是挺有用的,需要自己配置一下。需要手动新建一个空白的.log文件。

#此处使用python自带的logging模块,用来作为测试日志,记录测试中系统产生的信息。
import logging,os
log_file = os.path.join(os.getcwd(),'log/sas.log')
log_format = '[%(asctime)s] [%(levelname)s] %(message)s' #配置log格式
logging.basicConfig(format=log_format, filename=log_file, filemode='w', level=logging.DEBUG)
console = logging.StreamHandler()
console.setLevel(logging.DEBUG)
formatter = logging.Formatter(log_format)
console.setFormatter(formatter)
logging.getLogger('').addHandler(console)

excel文件如下图

python读取excel跟读取一个二维数组差不多,下标也是从0开始

#读取testcase excel文件,获取测试数据,调用interfaceTest方法,将结果保存至errorCase列表中。
import xlrd,hashlib,json

def runTest(testCaseFile):
testCaseFile = os.path.join(os.getcwd(),testCaseFile)
if not os.path.exists(testCaseFile):
logging.error('测试用例文件不存在!')
sys.exit()
testCase = xlrd.open_workbook(testCaseFile)
table = testCase.sheet_by_index(0)
errorCase = [] #用于保存接口返回的内容和HTTP状态码

s = None
for i in range(1,table.nrows):
if table.cell(i, 9).vale.replace('\n','').replace('\r','') != 'Yes':
continue
num = str(int(table.cell(i, 0).value)).replace('\n','').replace('\r','')
api_purpose = table.cell(i, 1).value.replace('\n','').replace('\r','')
api_host = table.cell(i, 2).value.replace('\n','').replace('\r','')
request_method = table.cell(i, 4).value.replace('\n','').replace('\r','')
request_data_type = table.cell(i, 5).value.replace('\n','').replace('\r','')
request_data = table.cell(i, 6).value.replace('\n','').replace('\r','')
encryption = table.cell(i, 7).value.replace('\n','').replace('\r','')
check_point = table.cell(i, 8).value

if encryption == 'MD5': #如果数据采用md5加密,便先将数据加密
request_data = json.loads(request_data)
request_data['pwd'] = md5Encode(request_data['pwd'])
status, resp, s = interfaceTest(num, api_purpose, api_host, request_url, request_data, check_point, request_methon, request_data_type, s)
if status != 200 or check_point not in resp: #如果状态码不为200或者返回值中没有检查点的内容,那么证明接口产生错误,保存错误信息。
errorCase.append((num + ' ' + api_purpose, str(status), 'http://'+api_host+request_url, resp))
return errorCase


下面的就是接口部分
由于所有的操作必须在系统登录之后进行,一开始没有注意到cookie这一点,每读取一个测试用例,都会新建一个session,导致无法维护上一次请求的cookie。然后将cookie添加入请求头中,但是第二个用例仍然无法执行成功。后来用fiddler抓包分析了一下,发现cookie的值竟然是每一次操作后都会变化的!!!

所以只能通过session自动维护cookie。
在interfaceTest函数中,返回三个值,分别是HTTP CODE,HTTP返回值与session。再将上一次请求的session作为入参传入interfaceTest函数中,在函数内部判断session是否存在,如果不为None,那么直接利用传入的session执行下一个用例,如果为None,那么新建一个session。

#接受runTest的传参,利用requests构造HTTP请求
import requests

def interfaceTest(num, api_purpose, api_host, request_method,
request_data_type, request_data, check_point, s=None)
headers = {'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With' : 'XMLHttpRequest',
'Connection' : 'keep-alive',
'Referer' : 'http://' + api_host,
'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36'
}

if s == None:
s = requests.session()
if request_method == 'POST':
if request_url != '/login' :
r = s.post(url='http://'+api_host+request_url, data = json.loads(request_data), headers = headers) #由于此处数据没有经过加密,所以需要把Json格式字符串解码转换成**Python对象**
elif request_url == '/login' :
s = requests.session()
r = s.post(url='http://'+api_host+request_url, data = request_data, headers = headers) #由于登录密码不能明文传输,采用MD5加密,在之前的代码中已经进行过json.loads()转换,所以此处不需要解码
else:
logging.error(num + ' ' + api_purpose + ' HTTP请求方法错误,请确认[Request Method]字段是否正确!!!')
s = None
return 400, resp, s
status = r.status_code
resp = r.text
print resp
if status == 200 :
if re.search(check_point, str(r.text)):
logging.info(num + ' ' + api_purpose + ' 成功,' + str(status) + ', ' + str(r.text))
return status, resp, s
else:
logging.error(num + ' ' + api_purpose + ' 失败!!!,[' + str(status) + '], ' + str(r.text))
return 200, resp , None
else:
logging.error(num + ' ' + api_purpose + ' 失败!!!,[' + str(status) + '],' + str(r.text))
return status, resp.decode('utf-8'), None

import hashlib

def md5Encode(data):
hashobj = hashlib.md5()
hashobj.update(data.encode('utf-8'))
return hashobj.hexdigest()

def sendMail(text):
mail_info = get_conf()
sender = mail_info['sender']
receiver = mail_info['receiver']
subject = '[AutomationTest]接口自动化测试报告通知'
smtpserver = mail_info['smtpserver']
username = mail_info['username']
password = mail_info['password']
msg = MIMEText(text,'html','utf-8')
msg['Subject'] = subject
msg['From'] = sender
msg['To'] = ''.join(receiver)
smtp = smtplib.SMTP()
smtp.connect(smtpserver)
smtp.login(username, password)
smtp.sendmail(sender, receiver, msg.as_string())
smtp.quit()


def main():
errorTest = runTest('TestCase/TestCase.xlsx')
if len(errorTest) > 0:
html = '<html><body>接口自动化扫描,共有 ' + str(len(errorTest)) + ' 个异常接口,列表如下:' + '</p><table><tr><th style="width:100px;text-align:left">接口</th><th style="width:50px;text-align:left">状态</th><th style="width:200px;text-align:left">接口地址</th><th style="text-align:left">接口返回值</th></tr>'
for test in errorTest:
html = html + '<tr><td style="text-align:left">' + test[0] + '</td><td style="text-align:left">' + test[1] + '</td><td style="text-align:left">' + test[2] + '</td><td style="text-align:left">' + test[3] + '</td></tr>'
sendMail(html)

if __name__ == '__main__':
main()

以上就是一个简单的接口测试实现,参考了论坛里的大神们很多东西,还有很多不足之处。例如html格式的邮件很简陋,可以考虑生成一个HTML的测试报告,作为附件发送。
接口方法只实现了一个POST。
编码也很随意,基本想到哪写到哪,不规范的地方请指正。

共收到 43 条回复 时间 点赞

学习了

—— 来自TesterHome官方 安卓客户端

不错,我觉得比我写的好

adonisjph #3 · May 21, 2016 作者

#2楼 @xiaoli 互相学习,很简陋的东西

最近我也在写接口测试的框架,图是我写的测试报告,我自己觉得长的还不错,哈哈!楼主可以参考一下

测试报告

#4楼 @wyb199026 看上去像是用bootstrap

#5楼 @zsx10110 是的,用bootstrap写起来比较快

adonisjph #7 · May 21, 2016 作者

#4楼 @wyb199026 ……这么一比,不对,是完全不能比啊,赞赞赞,很漂亮的测试报告

学习了,谢谢分享

—— 来自TesterHome官方 安卓客户端

#7楼 @jphtmt 谢谢称赞,这个就是bootstrap的demo拼起来的,我用Django一起弄出来的,把测试结果用txt文本作为缓存,最后生成这个报告之后直接用爬虫爬下来保存本地,这样历史记录也都可以查了

我最近也是在找接口测试这方面的,看了你这个很有帮助啊。
我这边自己写的流程是:
1.从excel读取URL和param
2.接受服务器返回的数据,判断数据的数量、类型
3.判断数据的正确性(准备使用正则判断,还没实现)
总的来说获取数据不是太难,关键是拿到数据后如何去操作。然后还有就是报告生成吧。

adonisjph #11 · May 23, 2016 作者

#10楼 @linbenyuan 你的流程跟我基本一致,数据正确性我也是使用正则判断,但是我的正则写的很简单,拿到数据后就看你的要求了,我只是跟预期的结果进行比对,如果不正确就返回错误,添加到错误的列表中。
报告的生成可以参见楼上用的bootstrap,但是感觉有点小复杂,我准备用个pyh写个简单的稍微漂亮点的页面。

if status == 200 :
if re.search(check_point, str(r.text)):
logging.info(num + ' ' + api_purpose + ' 成功,' + str(status) + ', ' + str(r.text))
return status, resp, s
else:
logging.error(num + ' ' + api_purpose + ' 失败!!!,[' + str(status) + '], ' + str(r.text))
return 200, resp , None
else:
logging.error(num + ' ' + api_purpose + ' 失败!!!,[' + str(status) + '],' + str(r.text))
return status, resp.decode('utf-8'), None

第二个else 缩进有问题吧

我真的很不明白,为什么大多数人要用excel管理测试数据
太繁琐了

adonisjph #14 · May 30, 2016 作者

#13楼 @jacexh 其实我也没有多考虑,就是看之前有人这么做的,就这么做了,你有什么更好的方式吗,分享一下

adonisjph #15 · May 30, 2016 作者

#12楼 @onecd 恩,我没注意,你看下代码,我已经修改好了

建议使用nose或者pytest等python主流的测试框架,直接使用参数--junit-xml方法导出标准的junit类型的测试报告,然后结合jenkins的report插件,就可以展示了

adonisjph #18 · June 01, 2016 作者

#17楼 @cesc nose和pytest是单元测试框架吧

感谢楼主,最近也在看这方面的😄

adonisjph #20 · June 03, 2016 作者

#19楼 @thanksdanny 希望能帮到你

挺好的,赞!!!

—— 来自TesterHome官方 安卓客户端

#14楼 @jphtmt pyh是啥,htmlrunner?

#4楼 @wyb199026 csdn好像有见过层主的教学,不知道层主是不是叫IT印记~

然后非常感谢楼主@jphtmt 为我指了一条明路~~~~

#23楼 @yiyusixing 额,是的。那是我学习过程的笔记

adonisjph #25 · June 13, 2016 作者

#22楼 @mads 是一个写html文件的库

adonisjph #26 · June 13, 2016 作者

#23楼 @yiyusixing 我也是参考了别人,自己整理的一点小东西,互相学习吧

#26楼 @jphtmt 您qq多少,我能问点问题么

adonisjph #28 · June 13, 2016 作者

#27楼 @mads 见我账号

报告生成推荐用 HTMLTestRunner. 可以百度一下. 很不错的东西


额 这两块参数是不是写错了啊

adonisjph #32 · July 07, 2016 作者

#31楼 @klxiaoqi 没有,re是一个包,导入后进行正则的。resp是http响应

可以加个qq吗 看你账号没找到呢

adonisjph #34 · July 08, 2016 作者

#33楼 @klxiaoqi 573980664

你好,我目前也在做接口测试这一块,刚入门,遇到一个问题,还望给点思路!
测试数据存放于excel,sheet与接口名对应,一个sheet页一个接口,每个sheet页多条测试数据
请问这种情况下 如何与单元测试结合,如何查看测试结果?望回复

学习啦~~帮我入门,感谢楼主的分享😁 😁

请教下,python读取接口Excel文件,如果接口请求参数为多条的话,对参数格式有要求吗,我目前这样写去请求,接口不返回数据

adonisjph #38 · March 31, 2017 作者
miaoyan 回复

得看你的接口是怎么解析的了,如果是按照json解析的, 那么传入标准格式的json就好。
最好在写之前,先手动发一些请求到接口看看是怎么样的情况,然后确定自己的数据格式。

Author only
adonisjph · #40 · May 04, 2018 作者
Author only
点点寒彬 回复

报告挺清晰的,就是左右两边的,执行的用例和结果还需要说清楚一下,第一眼没看出来

大佬,请教一下,为什么大部分人封装requests都会封装json.loads()?为什么不先判断返回的状态码是否正确?还有您用例里考虑的这几个参数就足够了吗,会不会有一些参数没有考虑进去啊

红客联盟 回复

看情况的,用到json格式的数据,就会用到json.loads()方法。
不先判断返回码是因为返回码不一定准确,比如有的系统,当查询不到结果的时候,也会返回200,然后内容是 无结果 这样,所以当你对接口返回有很清楚的定义时,就直接判断内容就好

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up