接口自动化测试方案详解

来源:互联网 发布:剑指offer 知乎 编辑:程序博客网 时间:2024/04/29 02:42

前言

去年,我们进行了项目的拆分,拆分后的各个子系统也都逐步的改成了通过接口进行数据的交换,接口测试也被提上日程。经过一段时间的探索,接口自动化测试方案越来越完善,今天给大家做个详细的讲解。

方案

目前我们的接口都是使用的http协议,其测试的基本原理是模拟前端(客户端)向服务器发送数据,得到相应的响应数据,从而判断接口是否可以正常的进行数据交换。在测试的过程中尝试过两种方式,一种是利用性能测试工具Jmeter模拟客户端发起http请求,另外一种是使用python脚本直接编写脚本模拟客户端发起http请求。

利用Jmeter工具配置,需要对如何利用Jmeter进行性能测试熟悉,通过相应的配置可完成,但不够灵活,比如某些字段需要经过特定的加密处理,不能通过Jmeter直接完成。

所以选择直接用python脚本进行,模拟http请求也就几行代码就可完成。但只是模拟请求不是最终的目标,也需要易用,不会编码的人也会维护我们的测试用例,所以形成了现在的形态,遵循了测试框架的一些基本原则,业务逻辑与测试脚本分离,测试脚本与测试数据分离。大致框架如下图所示:

image001

目录结构如下:

image002

所有的测试用例使用Excel统一管理,测试数据根据需要可以选择配置在Excel中或者保存在测试数据文件中。测试用例格式如下:

image003 image006 image007

日志格式如下:

image010

测试完成后可将异常的接口通过邮件发送给相关人。以上是接口测试方案的大致介绍,下面给大家说说具体怎么配置用例。

如何进行测试

测试的核心脚本已经搭建好,后续不会有太大的改动,维护测试用例的Excel表格即可完成后续接口的测试,不管是新接口的测试还是老接口的回归,那如何编写一个接口的测试用例呢?

1、      打开测试用例的Excel表格,填写用例编号、接口描述信息,被测接口的域名和请求地址。

image011

2、      选择接口请求的方式,目前有两种,一种是POST,一种是GET,根据实际情况选择。

image013

3、      选择接口接收数据的方式,目前有三种,Form类型,请求的数据会进行urlencode编码,一般都是这种类型,官网的接口主要是这种;Data类型,以文本的形式直接请求接口,不经过urlencode编码,引擎的接口大部分是这种,选择Data类型时,请求的数据有两种,一种是直接在Excel中配置json字符串,一种是填写文本文件路径,文件中也是json字符串,主要在于post的数据很大时,比如保存案例,在Excel中不好管理。File类型表示上传文件,在测试上传时选择File类型。

image014

4、      配置需要向接口发送的数据,如下图所示,需要根据上一步中选择的类型配置正确的测试数据,除了填写的是文件路径外,数据必须是标准的json格式字符串。

image015

测试数据中,可以带参数,格式为${parameter},此处的参数必须在后面的关联(Correlation)字段中有赋值,在后面的关联字段配置给大家详细介绍。其中内置了四个参数,分别是:${randomEmail}(随机邮箱地址)、${randomTel}(随机手机号码)、${timestamp}(当前时间戳)、${session}(session id,默认为None)以及${hashPassword}(hash加密密码,明文123456)。

5、      配置数据是否需要编码加密,目前有三种,不加密,MD5加密和DES加密。这是根据我们自身项目的特点加的选项,引擎有几个接口需要进行MD5加密,场景秀的接口都经过了DES加密。

image016

6、      配置检查点,检查点的目的是校验接口返回的数据是否是我们期望的。

image017

7、      配置关联,在接口的测试过程中,两个接口常常会有相关性,比如引擎新建案例需要先登录官网,那么,就需要做前后接口数据的关联。前面步骤已经提到过,在配置测试数据的时候可以配置参数,那么,关联的配置就是为了给这些参数赋值的,格式如下:${parameter}=[level1][level2][level3],多个参数中间用半角的分号(;)隔开,如下图所示。关联参数有两部分组成,等号前面是参数名称,需要跟测试数据中配置的参数名称保持一致,等号后面的部分是获取当前接口返回值的,因为接口返回值都是json格式的字符串,所以[level1]表示第一层级的指定key的值,[level1][level2]表示获取第一层级指定key的值中的指定key的值,有点绕,我们举例说明,大家就明白了。

image018

登录接口的返回值是:

  1. {"data":"http:\/\/my.test.liveapp.com.cn\/admin\/myapp\/applist","success":true,"message":"6tdehonrs6mg9metjqprfind16"}

后续的操作都需要是登录状态,所以需要得到session id,那么参数就可以这么写:${session}=[message],得到的值就是6tdehonrs6mg9metjqprfind16。

保存案例接口的返回值是:

  1. {"ecode":0,"msg":"SUCCESS","data":[{"$id":"55d43d077f8b9ad56b8b4576","page_id":115323,"page_order":0},……

后续的操作需要mongo id和page id,那么参数可以这样写:${mongo_id}=[data][0][$id];${page_id}=[data][0][page_id],就可以分别得到55d43d077f8b9ad56b8b4576和115323。这里大家发现会出现数字,是因为”data”的值是一个列表,而不是字典,没有相应的key,所以可以用数字代替,从0开始计算。

8、      最后一步,配置用例是否执行,只有Yes和No两种选项,这个很好理解,就不多解释了。

image019

以上就是配置一条用例的过程,配置完成后,保存Excel文件,提交到SVN即可,Jenkins接口测试的项目已经配置好,在每次引擎项目构建之后都会自动构建接口测试项目。

如果大家还有什么疑问,可以找我一起探讨。

附代码如下(Github:https://github.com/TronGeek/InterfaceTest):

  1. #!/usr/bin/env python
  2. #coding=utf8
  3. # Todo:接口自动化测试
  4. # Author:归根落叶
  5. # Blog:http://this.ispenn.com
  6. import json
  7. import http.client,mimetypes
  8. from urllib.parse import urlencode
  9. import random
  10. import time
  11. import re
  12. import logging
  13. import os,sys
  14. try:
  15. import xlrd
  16. except:
  17. os.system('pip install -U xlrd')
  18. import xlrd
  19. try:
  20. from pyDes import *
  21. except ImportError as e:
  22. os.system('pip install -U pyDes --allow-external pyDes --allow-unverified pyDes')
  23. from pyDes import *
  24. import hashlib
  25. import base64
  26. import smtplib
  27. from email.mime.text import MIMEText
  28. log_file = os.path.join(os.getcwd(),'log/liveappapi.log')
  29. log_format ='[%(asctime)s] [%(levelname)s] %(message)s'
  30. logging.basicConfig(format=log_format,filename=log_file,filemode='w',level=logging.DEBUG)
  31. console = logging.StreamHandler()
  32. console.setLevel(logging.DEBUG)
  33. formatter = logging.Formatter(log_format)
  34. console.setFormatter(formatter)
  35. logging.getLogger('').addHandler(console)
  36. #获取并执行测试用例
  37. def runTest(testCaseFile):
  38. testCaseFile = os.path.join(os.getcwd(),testCaseFile)
  39. if not os.path.exists(testCaseFile):
  40. logging.error('测试用例文件不存在!!!')
  41. sys.exit()
  42. testCase = xlrd.open_workbook(testCaseFile)
  43. table = testCase.sheet_by_index(0)
  44. errorCase = []
  45. correlationDict = {}
  46. correlationDict['${hashPassword}'] = hash1Encode('123456')
  47. correlationDict['${session}'] = None
  48. for i in range(1,table.nrows):
  49. correlationDict['${randomEmail}'] = ''.join(random.sample('abcdefghijklmnopqrstuvwxyz',6)) + '@automation.test'
  50. correlationDict['${randomTel}'] = '186' + str(random.randint(10000000,99999999))
  51. correlationDict['${timestamp}'] = int(time.time())
  52. if table.cell(i,10).value.replace('\n','').replace('\r','') != 'Yes':
  53. continue
  54. num = str(int(table.cell(i,0).value)).replace('\n','').replace('\r','')
  55. api_purpose = table.cell(i,1).value.replace('\n','').replace('\r','')
  56. api_host = table.cell(i,2).value.replace('\n','').replace('\r','')
  57. request_url = table.cell(i,3).value.replace('\n','').replace('\r','')
  58. request_method = table.cell(i,4).value.replace('\n','').replace('\r','')
  59. request_data_type = table.cell(i,5).value.replace('\n','').replace('\r','')
  60. request_data = table.cell(i,6).value.replace('\n','').replace('\r','')
  61. encryption = table.cell(i,7).value.replace('\n','').replace('\r','')
  62. check_point = table.cell(i,8).value
  63. correlation = table.cell(i,9).value.replace('\n','').replace('\r','').split(';')
  64. for key in correlationDict:
  65. if request_url.find(key) > 0:
  66. request_url = request_url.replace(key,str(correlationDict[key]))
  67. if request_data_type == 'Form':
  68. dataFile = request_data
  69. if os.path.exists(dataFile):
  70. fopen = open(dataFile,encoding='utf-8')
  71. request_data = fopen.readline()
  72. fopen.close()
  73. for keyword in correlationDict:
  74. if request_data.find(keyword) > 0:
  75. request_data = request_data.replace(keyword,str(correlationDict[keyword]))
  76. try:
  77. if encryption == 'MD5':
  78. request_data = json.loads(request_data)
  79. status,md5 = getMD5(api_host,urlencode(request_data).replace("%27","%22"))
  80. if status != 200:
  81. logging.error(num +' ' + api_purpose + "[ " + str(status) + " ], 获取md5验证码失败!!!")
  82. continue
  83. request_data = dict(request_data,**{"sign":md5.decode("utf-8")})
  84. request_data = urlencode(request_data).replace("%27","%22")
  85. elif encryption == 'DES':
  86. request_data = json.loads(request_data)
  87. request_data = urlencode({'param':encodePostStr(request_data)})
  88. else:
  89. request_data = urlencode(json.loads(request_data))
  90. except Exception as e:
  91. logging.error(num +' ' + api_purpose + ' 请求的数据有误,请检查[Request Data]字段是否是标准的json格式字符串!')
  92. continue
  93. elif request_data_type == 'Data':
  94. dataFile = request_data
  95. if os.path.exists(dataFile):
  96. fopen = open(dataFile,encoding='utf-8')
  97. request_data = fopen.readline()
  98. fopen.close()
  99. for keyword in correlationDict:
  100. if request_data.find(keyword) > 0:
  101. request_data = request_data.replace(keyword,str(correlationDict[keyword]))
  102. request_data = request_data.encode('utf-8')
  103. elif request_data_type == 'File':
  104. dataFile = request_data
  105. if not os.path.exists(dataFile):
  106. logging.error(num +' ' + api_purpose + ' 文件路径配置无效,请检查[Request Data]字段配置的文件路径是否存在!!!')
  107. continue
  108. fopen = open(dataFile,'rb')
  109. data = fopen.read()
  110. fopen.close()
  111. request_data ='''
  112. ------WebKitFormBoundaryDf9uRfwb8uzv1eNe
  113. Content-Disposition:form-data;name="file";filename="%s"
  114. Content-Type:
  115. Content-Transfer-Encoding:binary
  116. %s
  117. ------WebKitFormBoundaryDf9uRfwb8uzv1eNe--
  118. ''' % (os.path.basename(dataFile),data)
  119. status,resp = interfaceTest(num,api_purpose,api_host,request_url,request_data,check_point,request_method,request_data_type,correlationDict['${session}'])
  120. if status != 200:
  121. errorCase.append((num +' ' + api_purpose,str(status),'http://'+api_host+request_url,resp))
  122. continue
  123. for j in range(len(correlation)):
  124. param = correlation[j].split('=')
  125. if len(param) == 2:
  126. if param[1] == '' or not re.search(r'^\[',param[1]) or not re.search(r'\]$',param[1]):
  127. logging.error(num +' ' + api_purpose + ' 关联参数设置有误,请检查[Correlation]字段参数格式是否正确!!!')
  128. continue
  129. value = resp
  130. for key in param[1][1:-1].split(']['):
  131. try:
  132. temp = value[int(key)]
  133. except:
  134. try:
  135. temp = value[key]
  136. except:
  137. break
  138. value = temp
  139. correlationDict[param[0]] = value
  140. return errorCase
  141. # 接口测试
  142. def interfaceTest(num,api_purpose,api_host,request_url,request_data,check_point,request_method,request_data_type,session):
  143. headers = {'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8',
  144. 'X-Requested-With':'XMLHttpRequest',
  145. 'Connection':'keep-alive',
  146. 'Referer':'http://' + api_host,
  147. 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36'}
  148. if session is not None:
  149. headers['Cookie'] = 'session=' + session
  150. if request_data_type == 'File':
  151. headers['Content-Type'] = 'multipart/form-data;boundary=----WebKitFormBoundaryDf9uRfwb8uzv1eNe;charset=UTF-8'
  152. elif request_data_type == 'Data':
  153. headers['Content-Type'] = 'text/plain; charset=UTF-8'
  154. conn = http.client.HTTPConnection(api_host)
  155. if request_method == 'POST':
  156. conn.request('POST',request_url,request_data,headers=headers)
  157. elif request_method == 'GET':
  158. conn.request('GET',request_url+'?'+request_data,headers=headers)
  159. else:
  160. logging.error(num +' ' + api_purpose + ' HTTP请求方法错误,请确认[Request Method]字段是否正确!!!')
  161. return 400,request_method
  162. response = conn.getresponse()
  163. status = response.status
  164. resp = response.read()
  165. if status == 200:
  166. resp = resp.decode('utf-8')
  167. if re.search(check_point,str(resp)):
  168. logging.info(num +' ' + api_purpose + ' 成功, ' + str(status) + ', ' + str(resp))
  169. return status,json.loads(resp)
  170. else:
  171. logging.error(num +' ' + api_purpose + ' 失败!!!, [ ' + str(status) + ' ], ' + str(resp))
  172. return 2001,resp
  173. else:
  174. logging.error(num +' ' + api_purpose + ' 失败!!!, [ ' + str(status) + ' ], ' + str(resp))
  175. return status,resp.decode('utf-8')
  176. #获取md5验证码
  177. def getMD5(url,postData):
  178. headers = {'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8',
  179. 'X-Requested-With':'XMLHttpRequest'}
  180. conn = http.client.HTTPConnection('this.ismyhost.com')
  181. conn.request('POST','/get_isignature',postData,headers=headers)
  182. response = conn.getresponse()
  183. return response.status,response.read()
  184. # hash1加密
  185. def hash1Encode(codeStr):
  186. hashobj = hashlib.sha1()
  187. hashobj.update(codeStr.encode('utf-8'))
  188. return hashobj.hexdigest()
  189. # DES加密
  190. def desEncode(desStr):
  191. k = des('secretKEY', padmode=PAD_PKCS5)
  192. encodeStr = base64.b64encode(k.encrypt(json.dumps(desStr)))
  193. return encodeStr
  194. # 字典排序
  195. def encodePostStr(postData):
  196. keyDict = {'key':'secretKEY'}
  197. mergeDict = dict(postData, **keyDict)
  198. mergeDict = sorted(mergeDict.items())
  199. postStr =''
  200. for i in mergeDict:
  201. postStr = postStr + i[0] +'=' + i[1] + '&'
  202. postStr = postStr[:-1]
  203. hashobj = hashlib.sha1()
  204. hashobj.update(postStr.encode('utf-8'))
  205. token = hashobj.hexdigest()
  206. postData['token'] = token
  207. return desEncode(postData)
  208. #发送通知邮件
  209. def sendMail(text):
  210. sender ='no-reply@myhost.cn'
  211. receiver = ['penn@myhost.cn']
  212. mailToCc = ['penn@myhost.cn']
  213. subject ='[AutomantionTest]接口自动化测试报告通知'
  214. smtpserver ='smtp.exmail.qq.com'
  215. username ='no-reply@myhost.cn'
  216. password ='password'
  217. msg = MIMEText(text,'html','utf-8')
  218. msg['Subject'] = subject
  219. msg['From'] = sender
  220. msg['To'] = ';'.join(receiver)
  221. msg['Cc'] = ';'.join(mailToCc)
  222. smtp = smtplib.SMTP()
  223. smtp.connect(smtpserver)
  224. smtp.login(username, password)
  225. smtp.sendmail(sender, receiver + mailToCc, msg.as_string())
  226. smtp.quit()
  227. def main():
  228. errorTest = runTest('TestCase/TestCasePre.xlsx')
  229. if len(errorTest) > 0:
  230. html ='接口自动化定期扫描,共有 ' + str(len(errorTest)) + ' 个异常接口,列表如下:' + ''
  231. for test in errorTest:
  232. html = html +''
  233. html = html +'<table><tbody><tr><th style="width:100px;">接口</th><th style="width:50px;">状态</th><th style="width:200px;">接口地址</th><th>接口返回值</th></tr><tr><td>' + test[0] + '</td><td>' + test[1] + '</td><td>' + test[2] + '</td><td>' + test[3] + '</td></tr></tbody></table>'
  234. #sendMail(html)
  235. if __name__ == '__main__':
  236. main()
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 教师职称证丢了怎么办 会计初级证丢了怎么办 工作遭同事不满否认质疑怎么办 单位领导不让进收入财务怎么办 事业单位50岁不愿退休的怎么办 回美国i20丢了怎么办 i20忘签字美国入境怎么办 社保基数报错了怎么办 公司合同没给我怎么办 给客户报错价格怎么办 给客户报价低了怎么办 报价失误报低了怎么办 期望薪资说低了怎么办 期望薪资说高了怎么办 面试工资说低了怎么办 期望薪资谈低了怎么办 请年假公司不批怎么办 期望工资填低了怎么办 面试工资要高了怎么办 找工作期望薪资写低了怎么办 期望工资写少了怎么办 不给工人发工资怎么办 天亮了怎么办我好想你 亲爱的我想你我怎么办 人在澳大利亚悉尼找不到了怎么办 红米手机忘记手势密码怎么办 捡到苹果手机怎么办才能自己用 日语会读不会写怎么办 手术后nbp过低怎么办 我的手破了怎么办英文 平板手机屏坏了怎么办 他很优秀我该怎么办 洗澡的花洒漏水怎么办 高三了文科成绩很差怎么办 骑缝章最后一页没盖全怎么办 机票取早了没有登机口怎么办 机票早订比晚订贵怎么办? 孩子考差了父母怎么办 保险公司不给业务员办退司怎么办 我不习惯没有你我怎么办 锁坏了打不开了怎么办