方案
目前我们的接口都是使用的http协议,其测试的基本原理是模拟前端(客户端)向服务器发送数据,得到相应的响应数据,从而判断接口是否可以正常的进行数据交换。在测试的过程中尝试过两种方式,一种是利用性能测试工具Jmeter模拟客户端发起http请求,另外一种是使用python脚本直接编写脚本模拟客户端发起http请求。
利用Jmeter工具配置,需要对如何利用Jmeter进行性能测试熟悉,通过相应的配置可完成,但不够灵活,比如某些字段需要经过特定的加密处理,不能通过Jmeter直接完成。
所以选择直接用python脚本进行,模拟http请求也就几行代码就可完成。但只是模拟请求不是最终的目标,也需要易用,不会编码的人也会维护我们的测试用例,所以形成了现在的形态,遵循了测试框架的一些基本原则,业务逻辑与测试脚本分离,测试脚本与测试数据分离。大致框架如下图所示:
目录结构如下:
所有的测试用例使用Excel统一管理,测试数据根据需要可以选择配置在Excel中或者保存在测试数据文件中。测试用例格式如下:
日志格式如下:
测试完成后可将异常的接口通过邮件发送给相关人。以上是接口测试方案的大致介绍,下面给大家说说具体怎么配置用例。
如何进行测试
测试的核心脚本已经搭建好,后续不会有太大的改动,维护测试用例的Excel表格即可完成后续接口的测试,不管是新接口的测试还是老接口的回归,那如何编写一个接口的测试用例呢?
1、 打开测试用例的Excel表格,填写用例编号、接口描述信息,被测接口的域名和请求地址。
2、 选择接口请求的方式,目前有两种,一种是POST,一种是GET,根据实际情况选择。
3、 选择接口接收数据的方式,目前有三种,
Form类型,请求的数据会进行urlencode编码,一般都是这种类型,官网的接口主要是这种;
Data类型,以文本的形式直接请求接口,不经过urlencode编码,引擎的接口大部分是这种,选择Data类型时,请求的数据有两种,一种是直接在Excel中配置json字符串,一种是填写文本文件路径,文件中也是json字符串,主要在于post的数据很大时,比如保存案例,在Excel中不好管理。File类型表示上传文件,在测试上传时选择File类型。
4、 配置需要向接口发送的数据,如下图所示,需要根据上一步中选择的类型配置正确的测试数据,除了填写的是文件路径外,数据必须是标准的json格式字符串。
测试数据中,可以带参数,格式为${parameter},此处的参数必须在后面的关联(Correlation)字段中有赋值,在后面的关联字段配置给大家详细介绍。其中内置了四个参数,分别是:${randomEmail}(随机邮箱地址)、${randomTel}(随机手机号码)、${timestamp}(当前时间戳)、${session}(session id,默认为None)以及${hashPassword}(hash加密密码,明文123456)。
5、 配置数据是否需要编码加密,目前有三种,不加密,MD5加密和DES加密。这是根据我们自身项目的特点加的选项,引擎有几个接口需要进行MD5加密,场景秀的接口都经过了DES加密。
6、 配置检查点,检查点的目的是校验接口返回的数据是否是我们期望的。
7、 配置关联,在接口的测试过程中,两个接口常常会有相关性,比如引擎新建案例需要先登录官网,那么,就需要做前后接口数据的关联。前面步骤已经提到过,在配置测试数据的时候可以配置参数,那么,关联的配置就是为了给这些参数赋值的,格式如下:${parameter}=[level1][level2][level3],多个参数中间用半角的分号(;)隔开,如下图所示。关联参数有两部分组成,等号前面是参数名称,需要跟测试数据中配置的参数名称保持一致,等号后面的部分是获取当前接口返回值的,因为接口返回值都是json格式的字符串,所以[level1]表示第一层级的指定key的值,[level1][level2]表示获取第一层级指定key的值中的指定key的值,有点绕,我们举例说明,大家就明白了。
登录接口的返回值是:
- {"data":"http:\/\/my.test.liveapp.com.cn\/admin\/myapp\/applist","success":true,"message":"6tdehonrs6mg9metjqprfind16"}
后续的操作都需要是登录状态,所以需要得到session id,那么参数就可以这么写:${session}=[message],得到的值就是6tdehonrs6mg9metjqprfind16。
保存案例接口的返回值是:
- {"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两种选项,这个很好理解,就不多解释了。
以上就是配置一条用例的过程,配置完成后,保存Excel文件,提交到SVN即可,Jenkins接口测试的项目已经配置好,在每次引擎项目构建之后都会自动构建接口测试项目。
如果大家还有什么疑问,可以找我一起探讨。
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 = '[email protected]' 211 receiver = ['[email protected]'] 212 mailToCc = ['[email protected]'] 213 subject = '[AutomantionTest]接口自动化测试报告通知' 214 smtpserver = 'smtp.exmail.qq.com' 215 username = '[email protected]' 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()