1. 程式人生 > >接口自動化測試方案詳解

接口自動化測試方案詳解

system earch ply 找我 發現 safari todo 文件中 timestamp

前言

去年,我們進行了項目的拆分,拆分後的各個子系統也都逐步的改成了通過接口進行數據的交換,接口測試也被提上日程。經過一段時間的探索,接口自動化測試方案越來越完善,今天給大家做個詳細的講解。

方案

目前我們的接口都是使用的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接口測試的項目已經配置好,在每次引擎項目構建之後都會自動構建接口測試項目。

如果大家還有什麽疑問,可以找我一起探討。

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


#!/usr/bin/env python
#coding=utf8

# Todo:接口自動化測試
# Author:歸根落葉
# Blog:http://this.ispenn.com

import json
import http.client,mimetypes
from urllib.parse import urlencode
import random
import time
import re
import logging
import os,sys
try:
import xlrd
except:
os.system(‘pip install -U xlrd‘)
import xlrd
try:
from pyDes import *
except ImportError as e:
os.system(‘pip install -U pyDes --allow-external pyDes --allow-unverified pyDes‘)
from pyDes import *
import hashlib
import base64
import smtplib
from email.mime.text import MIMEText

log_file = os.path.join(os.getcwd(),‘log/liveappapi.log‘)
log_format = ‘[%(asctime)s] [%(levelname)s] %(message)s‘
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)

#獲取並執行測試用例
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 = []
correlationDict = {}
correlationDict[‘${hashPassword}‘] = hash1Encode(‘123456‘)
correlationDict[‘${session}‘] = None
for i in range(1,table.nrows):
correlationDict[‘${randomEmail}‘] = ‘‘.join(random.sample(‘abcdefghijklmnopqrstuvwxyz‘,6)) + ‘@automation.test‘
correlationDict[‘${randomTel}‘] = ‘186‘ + str(random.randint(10000000,99999999))
correlationDict[‘${timestamp}‘] = int(time.time())
if table.cell(i,10).value.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_url = table.cell(i,3).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
correlation = table.cell(i,9).value.replace(‘\n‘,‘‘).replace(‘\r‘,‘‘).split(‘;‘)
for key in correlationDict:
if request_url.find(key) > 0:
request_url = request_url.replace(key,str(correlationDict[key]))
if request_data_type == ‘Form‘:
dataFile = request_data
if os.path.exists(dataFile):
fopen = open(dataFile,encoding=‘utf-8‘)
request_data = fopen.readline()
fopen.close()
for keyword in correlationDict:
if request_data.find(keyword) > 0:
request_data = request_data.replace(keyword,str(correlationDict[keyword]))
try:
if encryption == ‘MD5‘:
request_data = json.loads(request_data)
status,md5 = getMD5(api_host,urlencode(request_data).replace("%27","%22"))
if status != 200:
logging.error(num + ‘ ‘ + api_purpose + "[ " + str(status) + " ], 獲取md5驗證碼失敗!!!")
continue
request_data = dict(request_data,**{"sign":md5.decode("utf-8")})
request_data = urlencode(request_data).replace("%27","%22")
elif encryption == ‘DES‘:
request_data = json.loads(request_data)
request_data = urlencode({‘param‘:encodePostStr(request_data)})
else:
request_data = urlencode(json.loads(request_data))
except Exception as e:
logging.error(num + ‘ ‘ + api_purpose + ‘ 請求的數據有誤,請檢查[Request Data]字段是否是標準的json格式字符串!‘)
continue
elif request_data_type == ‘Data‘:
dataFile = request_data
if os.path.exists(dataFile):
fopen = open(dataFile,encoding=‘utf-8‘)
request_data = fopen.readline()
fopen.close()
for keyword in correlationDict:
if request_data.find(keyword) > 0:
request_data = request_data.replace(keyword,str(correlationDict[keyword]))
request_data = request_data.encode(‘utf-8‘)
elif request_data_type == ‘File‘:
dataFile = request_data
if not os.path.exists(dataFile):
logging.error(num + ‘ ‘ + api_purpose + ‘ 文件路徑配置無效,請檢查[Request Data]字段配置的文件路徑是否存在!!!‘)
continue
fopen = open(dataFile,‘rb‘)
data = fopen.read()
fopen.close()
request_data = ‘‘‘
------WebKitFormBoundaryDf9uRfwb8uzv1eNe
Content-Disposition:form-data;name="file";filename="%s"
Content-Type:
Content-Transfer-Encoding:binary

%s
------WebKitFormBoundaryDf9uRfwb8uzv1eNe--
‘‘‘ % (os.path.basename(dataFile),data)
status,resp = interfaceTest(num,api_purpose,api_host,request_url,request_data,check_point,request_method,request_data_type,correlationDict[‘${session}‘])
if status != 200:
errorCase.append((num + ‘ ‘ + api_purpose,str(status),‘http://‘+api_host+request_url,resp))
continue
for j in range(len(correlation)):
param = correlation[j].split(‘=‘)
if len(param) == 2:
if param[1] == ‘‘ or not re.search(r‘^\[‘,param[1]) or not re.search(r‘\]$‘,param[1]):
logging.error(num + ‘ ‘ + api_purpose + ‘ 關聯參數設置有誤,請檢查[Correlation]字段參數格式是否正確!!!‘)
continue
value = resp
for key in param[1][1:-1].split(‘][‘):
try:
temp = value[int(key)]
except:
try:
temp = value[key]
except:
break
value = temp
correlationDict[param[0]] = value
return errorCase

# 接口測試
def interfaceTest(num,api_purpose,api_host,request_url,request_data,check_point,request_method,request_data_type,session):
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 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36‘}
if session is not None:
headers[‘Cookie‘] = ‘session=‘ + session
if request_data_type == ‘File‘:
headers[‘Content-Type‘] = ‘multipart/form-data;boundary=----WebKitFormBoundaryDf9uRfwb8uzv1eNe;charset=UTF-8‘
elif request_data_type == ‘Data‘:
headers[‘Content-Type‘] = ‘text/plain; charset=UTF-8‘

conn = http.client.HTTPConnection(api_host)
if request_method == ‘POST‘:
conn.request(‘POST‘,request_url,request_data,headers=headers)
elif request_method == ‘GET‘:
conn.request(‘GET‘,request_url+‘?‘+request_data,headers=headers)
else:
logging.error(num + ‘ ‘ + api_purpose + ‘ HTTP請求方法錯誤,請確認[Request Method]字段是否正確!!!‘)
return 400,request_method
response = conn.getresponse()
status = response.status
resp = response.read()
if status == 200:
resp = resp.decode(‘utf-8‘)
if re.search(check_point,str(resp)):
logging.info(num + ‘ ‘ + api_purpose + ‘ 成功, ‘ + str(status) + ‘, ‘ + str(resp))
return status,json.loads(resp)
else:
logging.error(num + ‘ ‘ + api_purpose + ‘ 失敗!!!, [ ‘ + str(status) + ‘ ], ‘ + str(resp))
return 2001,resp
else:
logging.error(num + ‘ ‘ + api_purpose + ‘ 失敗!!!, [ ‘ + str(status) + ‘ ], ‘ + str(resp))
return status,resp.decode(‘utf-8‘)

#獲取md5驗證碼
def getMD5(url,postData):
headers = {‘Content-Type‘:‘application/x-www-form-urlencoded; charset=UTF-8‘,
‘X-Requested-With‘:‘XMLHttpRequest‘}
conn = http.client.HTTPConnection(‘this.ismyhost.com‘)
conn.request(‘POST‘,‘/get_isignature‘,postData,headers=headers)
response = conn.getresponse()
return response.status,response.read()

# hash1加密
def hash1Encode(codeStr):
hashobj = hashlib.sha1()
hashobj.update(codeStr.encode(‘utf-8‘))
return hashobj.hexdigest()

# DES加密
def desEncode(desStr):
k = des(‘secretKEY‘, padmode=PAD_PKCS5)
encodeStr = base64.b64encode(k.encrypt(json.dumps(desStr)))
return encodeStr

# 字典排序
def encodePostStr(postData):
keyDict = {‘key‘:‘secretKEY‘}
mergeDict = dict(postData, **keyDict)
mergeDict = sorted(mergeDict.items())
postStr = ‘‘
for i in mergeDict:
postStr = postStr + i[0] + ‘=‘ + i[1] + ‘&‘
postStr = postStr[:-1]
hashobj = hashlib.sha1()
hashobj.update(postStr.encode(‘utf-8‘))
token = hashobj.hexdigest()
postData[‘token‘] = token
return desEncode(postData)

#發送通知郵件
def sendMail(text):
sender = ‘[email protected]‘
receiver = [‘[email protected]‘]
mailToCc = [‘[email protected]‘]
subject = ‘[AutomantionTest]接口自動化測試報告通知‘
smtpserver = ‘smtp.exmail.qq.com‘
username = ‘[email protected]‘
password = ‘password‘

msg = MIMEText(text,‘html‘,‘utf-8‘)
msg[‘Subject‘] = subject
msg[‘From‘] = sender
msg[‘To‘] = ‘;‘.join(receiver)
msg[‘Cc‘] = ‘;‘.join(mailToCc)
smtp = smtplib.SMTP()
smtp.connect(smtpserver)
smtp.login(username, password)
smtp.sendmail(sender, receiver + mailToCc, msg.as_string())
smtp.quit()

def main():
errorTest = runTest(‘TestCase/TestCasePre.xlsx‘)
if len(errorTest) > 0:
html = ‘接口自動化定期掃描,共有 ‘ + str(len(errorTest)) + ‘ 個異常接口,列表如下:‘ + ‘


for test in errorTest:
html = html + ‘


html = html + ‘

接口狀態接口地址接口返回值
‘ + test[0] + ‘ ‘ + test[1] + ‘ ‘ + test[2] + ‘ ‘ + test[3] + ‘


#sendMail(html)

if __name__ == ‘__main__‘:
main()

作者:歸根落葉, 轉自:https://www.ispenn.com/2015/08/interface-test-automation-scheme-details

接口自動化測試方案詳解