关于接口功能自动化的思考
一、背景
目前接触的业务涉及web、APP(Android&IOS)以及H5小程序等多终端,但只要业务仍是TO G端的业务,涉及的只要是web端操作测试。上次进行UI自动化回归测试,如果涉及到页面改动比较频繁,页面中某些关键字会经常变更,导致元素定位每个版本都需要复查并修正关键字的定位,这样会消耗大量的人力物力,但往往事倍功半。
二、方案对比
2.1 Postman
对于真正的接口自动化测试而言,需要少量的代码即可完成接口自动化。相对而言,postman语言结构统一、易掌握,可以快速上手投入实战。
但针对我们的业务情况,对于TO G系统,这里没有做到前后端分离,故不是纯正的后端接口,有大量的jsp页面杂糅在一起,这种情况下多方考虑使用APP端的接口方案替换;还有一个问题是业务有很多关联业务和步骤,比如一个业务步骤涉及A、B、C其中A步骤中涉及A1接口、A2接口、A3接口;B、C同理。
目前在postman中暂时未找到比较好的解决方案,而JMeter也面临同样的问题。
所以,使用postman处理了部分自动化,后来实在是太折腾了,放弃了此方案。
2.2 RobotFrameWork
RobotFrameWork应用更广泛,使用于web、app、以及接口,此前关于UI自动化曾使用RobotFrameWork完成自动化回归工作,但是问题时跑完整个模块需要消耗大量的时间,而且脚本变动也比较频繁。
2.3 Seldom
此前曾使用此python二次封装的框架完成UI自动化工作,对于此框架比较熟悉,多方验证使用seldom同样可以完成接口自动化。还有一个优点在于提高了自动化的效率,UI自动化跑完一个模块(300个左右用例)大概需要1天甚至更久,而使用接口自动化则只需要2个小时左右,下图截图所示
三、具体示例
处理的业务流程:
3.1 登录代码如下
# -*- coding:utf-8 -*-
import seldom
class LoginTest(seldom.TestCase):
"""登录APP操作"""
def start(self):
pass
def test_login(self):
# 登录请求接口url
self.login_event = "/login"
# 登录请求参数
payload ={
'mobileLogin':'1',
'username':'6bcce5f671fd3f27672c13feef54c8f550aeab495e1d04fba86120d2e99c1beed54258a70145d0371c3a738bb2dd1d9232fed6235f9f2e8f2722c974caf42fe4777c4d51237e3274473d5188367058931eafae68e5b9d70c74824d864467117dfb03b9b818d4646772adec9eadbf7044b43e329e44d38dff4e15459198237ecc',
'password':'7b54901eaa0e6b69ff603cb02a1aef7749567eeb5a61e5038e86eabccb7ba45d34e742572d70bb80ed8bb7e19f32fa9b9d93874b5419cb4512fe119c50878e4f126e53f67434c8a0441a57a65b6ea7df01e40352dfb95bc4e319f516ab42f662b569896f3d48e030793c65d31b4824cb55d8eb8fafd1c0092f8d1b47b0f125b2',
'accountSuitId':'fa80c8f8b9ee43a79fdfa5ddf8d29807',
'identifierId':'09eff7963d5c88c2',
'validateCode':'',
'loginType':'1'
}
"""Cookie: accountSuitId=fa80c8f8b9ee43a79fdfa5ddf8d29807"""
headers = {
# 从浏览器中复制过来的User-Agent
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36',
# 从浏览器中复制过来的Cookie
'Cookie': 'accountSuitId=fa80c8f8b9ee43a79fdfa5ddf8d29807'
}
# 登录请求接口
r_login = self.post(self.login_event,headers=headers,data=payload)
self.assertStatusCode(200)
login_response = self.response
print(type(login_response))
# 登录请求返回值
login_token = self.response['rcm.session.id']
print(login_token)
return login_token
def logout(self):
"""退出登录接口信息"""
self.logout_url = '/logout'
payload ={}
# 退出接口信息
r_userinfo = self.get(self.logout_url,data=payload)
self.assertStatusCode(200)
if __name__ == '__main__':
seldom.main()
3.2 个人信息接口代码
# -*- coding:utf-8 -*-
import seldom
from test_login import LoginTest
class UserInfoTest(seldom.TestCase):
""""获取个人登录信息"""
def setup(self):
pass
def test_user(self):
"""个人信息如下"""
self.user_url = '/app/user/infoData'
sign_login = LoginTest()
user_token = sign_login.test_login()
print(user_token)
Cookies = {
# 从浏览器中复制过来的Cookie
'rcm.session.id':user_token,
'accountSuitId':'fa80c8f8b9ee43a79fdfa5ddf8d29807'
}
# 个人信息接口
r_userinfo = self.get(self.user_url,cookies=Cookies)
self.assertStatusCode(200)
user_response = self.response
print(type(user_response))
# 获取个人信息接口返回信息
login_id = user_response['id']
loginName = user_response['name']
mobile = user_response['mobile']
admin = user_response['admin']
roleNames = user_response['roleNames']
accountSuitAdmin = user_response['accountSuitAdmin']
loginFlag = user_response['loginFlag']
jobGrade = user_response['jobGrade']
appLoginFlag = user_response['appLoginFlag']
officeId = user_response['officeId']
officeName = user_response['officeName']
print("login_id:"+login_id)
print("loginName:"+loginName)
print("mobile:"+mobile)
print("admin:"+str(admin))
print("roleNames:"+roleNames)
print("accountSuitAdmin:"+str(accountSuitAdmin))
print("loginFlag:"+loginFlag)
print("jobGrade:"+jobGrade)
print("appLoginFlag:"+appLoginFlag)
print("officeId:"+officeId)
print("officeName:"+officeName)
return login_id,loginName,mobile,admin,roleNames,accountSuitAdmin,loginFlag,jobGrade,appLoginFlag,officeId,officeName
if __name__ == '__main__':
seldom.main()
3.3 完整申请单流程代码
展示了具体的申请-->提交审批--->审批人登录系统并审批整个操作流程,代码如下:
# -*- coding:utf-8 -*-
import seldom
import json
import time
from datetime import date
import random
import string
from faker import Faker
from test_userinfo import UserInfoTest
from test_login import LoginTest
from test_approval_login import LoginApprovalTest
from test_common_before import SelectApplyCommon
class BeforeApplyTest(seldom.TestCase):
""""事前申请相关接口测试操作"""
def setup(self):
sign_login = LoginTest()
user_token = sign_login.test_login()
print(user_token)
return user_token
def test_before_apply_submit(self):
"""事前申请提交相关信息"""
# 登录,获取个人信息
user_info = UserInfoTest()
user_info_data = user_info.test_user()
user_info_data = list(user_info_data)
print(user_info_data)
login_id = user_info_data[0]
login_name = user_info_data[1]
mobile = user_info_data[2]
admin = user_info_data[3]
roleNames = user_info_data[4]
accountSuitAdmin = user_info_data[5]
loginFlag = user_info_data[6]
jobGrade = user_info_data[7]
appLoginFlag = user_info_data[8]
officeId = user_info_data[9]
officeName = user_info_data[10]
print(login_id)
"""第一步:获取预算指标信息,这里获取第一个指标"""
self.before_budget_url = '/budget/budget/selector/listData?state=1'
payload={
'type': '',
'controlType':'',
'freezeFlag':'',
'forwardFlag':'',
'tempFlag':'',
'delFlag':'0',
'source':'10',
'payProjectId':'',
'applyUserId':login_id,
'applyType':'101',
'hasBudgetFlag':'1',
'extIds':'',
'forAdjust':'',
'year':'2021',
'name':'测试20210309001',
'projectName':'',
'budgetProject.id':'',
'pageNo':'',
'pageSize':'',
'orderBy':''
}
Cookies = {
# 从浏览器中复制过来的Cookie
'rcm.session.id':self.setup(),
'accountSuitId':'fa80c8f8b9ee43a79fdfa5ddf8d29807'
}
# 获取预算相关信息接口
r_userinfo = self.post(self.before_budget_url,cookies=Cookies,data=payload)
self.assertStatusCode(200)
user_response = self.response
print(type(user_response))
"""#第一步:获取预算指标信息,获取预算信息相关返回值信息"""
indicators_project_id = user_response['list'][0]['id']
indicators_budgetProject_id = user_response['list'][1]['id']
indicators_name = user_response['list'][1]['name']
indicators_budgetProject_type = user_response['list'][1]['type']
indicators_budgetProject_payProject_id = user_response['list'][1]['payProject']['id']
indicators_budgetProject_payProject_name = user_response['list'][1]['payProject']['name']
indicators_budgetProject_money_id = user_response['list'][1]['moneySources'][0]['id']
""""打印list-1"""
print(user_response['list'][1])
print("indicators_project_id:"+indicators_project_id)
print("indicators_budgetProject_id:"+indicators_budgetProject_id)
print("indicators_name:"+indicators_name)
print("indicators_budgetProject_type:"+indicators_budgetProject_type)
print("indicators_budgetProject_payProject_id:"+indicators_budgetProject_payProject_id)
print("indicators_budgetProject_payProject_name:"+indicators_budgetProject_payProject_name)
print("indicators_budgetProject_money_id:"+indicators_budgetProject_money_id)
"""# 第二步:获取可用金额信息/budexec/beforeApply/getAvailableBalance"""
self.budget_get_alia_url = '/budexec/beforeApply/getAvailableBalance'
payload = {
'payProjectId': indicators_budgetProject_payProject_id,
'budgetId': indicators_budgetProject_id,
'projectId': indicators_project_id,
'applyOfficeId': officeId,
'applyUserId': login_id
}
# 获取预算可用金额接口
r_userinfo = self.post(self.budget_get_alia_url,cookies=Cookies,data=payload)
self.assertStatusCode(200)
budget_aliableMoney = self.response
print(type(budget_aliableMoney))
print(budget_aliableMoney)
"""# 第三步:获取预算项目支付可用金额,/budexec/beforeApply/payAvailableBalance"""
self.budget_get_pay_url = '/budexec/beforeApply/payAvailableBalance'
payload={
'budgetId': indicators_budgetProject_id,
'payProjectId': indicators_budgetProject_payProject_id,
'projectId': indicators_project_id
}
# 获取预算支付可用金额接口
r_userinfo = self.post(self.budget_get_pay_url,cookies=Cookies,data=payload)
self.assertStatusCode(200)
budget_PaliableMoney = self.response
print(type(budget_PaliableMoney))
print(budget_PaliableMoney)
"""第四步:获取指标明细信息/app/selector/beforePayProjectDetailsInfo"""
self.budget_detail_pay_url = '/app/selector/beforePayProjectDetailsInfo'
payload = {
"payProject": {
"id": indicators_budgetProject_payProject_id,
"name": indicators_budgetProject_payProject_name,
},
"availableBalance": budget_PaliableMoney,
"applyType": "101",
"budget": {
"type": indicators_budgetProject_type,
"name": indicators_name,
"id": indicators_budgetProject_id
},
"applyUser": {
"name": login_name,
"mobile": mobile,
"id":login_id,
"admin":admin,
"roleNames":roleNames,
"accountSuitAdmin":accountSuitAdmin,
"loginFlag": loginFlag,
"appLoginFlag": appLoginFlag,
"jobGrade":jobGrade,
},
}
# 先转成Json字符串
data = json.dumps(payload,ensure_ascii=False)
# 按照utf-8编码成字节码
data = data.encode("utf-8")
# 获取预算支出明细接口
r_userinfo = self.post(self.budget_detail_pay_url,cookies=Cookies,data=data,headers={'Content-Type': 'application/json;charset=UTF-8'})
self.assertStatusCode(200)
indicators_project = self.response
indicators_project_controlName = indicators_project['data']['details'][0]['controlName']
indicators_project_payProjectDetail_name = indicators_project['data']['details'][0]['payProjectDetail']['name']
indicators_project_payProjectDetail_id = indicators_project['data']['details'][0]['payProjectDetail']['id']
print(type(indicators_project))
print(indicators_project)
print(indicators_project_controlName)
print(indicators_project_payProjectDetail_name)
print(indicators_project_payProjectDetail_id)
""""第五步:上传附件/sys/sysFile/upload"""
self.file_upload_url = '/sys/sysFile/upload'
payload={
'businessCategory':'01a',
'fileConfigCode': '1',
'id': 'WU_FILE_0',
'name': 'vpn选择指南.pdf',
'type': 'application/pdf',
'lastModifiedDate': 'Sat Jul 03 2021 16:21:29 GMT+0800 (中国标准时间)',
'size': '1546424'}
files=[
('file',('05.负载均衡_linux_v1.docx',open('C:\\Users\\admin\\Postman\\files\\05.负载均衡_linux_v1.docx','rb'),'application/vnd.openxmlformats-officedocument.wordprocessingml.document'))
]
# 获取上传文件接口信息
r_userinfo = self.post(self.file_upload_url,cookies=Cookies,files=files,data=payload)
self.assertStatusCode(200)
indicators_project = self.response
file_id = indicators_project[0]
print(file_id)
""""第六步:提交事前申请/budexec/beforeApplyCommon/submit"""
self.budget_apply_submit_url = '/budexec/beforeApplyCommon/submit'
# 随机日期和字符串信息
# 格式化成2016-03-20 11:45:39形式
today_date = date.today()
print(today_date.strftime("%Y-%m-%d"))
apply_date = today_date.strftime("%Y-%m-%d")
localTime = time.strftime("%Y%m%d%H%M%S", time.localtime())
salt = ''.join(random.sample(string.ascii_letters + string.digits, 8))
rString = salt + localTime
# num = ''.join(random.sample(string.digits, 8))
# reveive_code = num + localTime
fake = Faker(locale='zh_CN')
abroad_num = fake.random_number(digits=3, fix_len=False)
apply_amount_temp = fake.pyfloat(left_digits=3, right_digits=2, positive=True)
apply_amount = str(apply_amount_temp)
exchange_rate_temp = fake.pyfloat(left_digits=1, right_digits=1, positive=True, min_value=0, max_value=1)
exchange_rate = str(exchange_rate_temp)
print(apply_amount)
payload={
'id': ' ',
'applyType': ' 101',
'applyOverFlag': ' 0',
'courseApplyId': ' ',
'isSubmit': ' ',
'generalReceiptFlag': ' ',
'affiliates[0].applyType': ' 101',
'affiliates[0].budget.type': ' 2',
'affiliates[0].beforeAddition.id': ' ',
'applyDate': apply_date,
'applyUser.id': login_id,
'applyUser.name': login_name,
'applyOffice.id': officeId,
'applyOffice.name': officeName,
'operatorUser.id': login_id,
'operatorUser.name': login_name,
'operatorOffice.id': officeId,
'operatorOffice.name': officeName,
'hasBudgetFlag': ' 1',
'affiliates[0].budgetAvailableBalance': budget_PaliableMoney,
'affiliates[0].budget.id': indicators_budgetProject_id,
'affiliates[0].budget.name': indicators_name,
'affiliates[0].applyAmount': apply_amount,
'affiliates[0].payProjectAvailableBalance': budget_PaliableMoney,
'affiliates[0].payProject.id': indicators_budgetProject_payProject_id,
'synthesizeFlag': ' 0',
'affiliates[0].payProject.name': indicators_budgetProject_payProject_name,
'affiliates[0].availableBalance': budget_aliableMoney,
'affiliates[0].projectAvailableBalance': budget_PaliableMoney,
'affiliates[0].project.id': indicators_project_id,
'affiliates[0].moneySource.id': indicators_budgetProject_money_id,
'applyReason': rString,
'affiliates[0].details[0].id': ' ',
'affiliates[0].details[0].payProjectDetail.id': indicators_project_payProjectDetail_id,
'affiliates[0].details[0].payProjectDetail.name': indicators_project_payProjectDetail_name,
'affiliates[0].details[0].payProjectDetail.isList': ' 0',
'affiliates[0].details[0].overFlag': ' 0',
'affiliates[0].details[0].controlAmount': ' ',
'affiliates[0].details[0].controlDescription': ' ',
'affiliates[0].details[0].controlName': indicators_project_controlName,
'affiliates[0].details[0].applyAmount': apply_amount,
'affiliates[0].details[0].remarks': rString,
'f4e714b005f94389a6b55f4237658b3f': ' ',
'fileIds': file_id,
'01a': ' f4e714b005f94389a6b55f4237658b3f',
'file': ''}
files=[
]
# 获取提交事前申请接口信息
r_userinfo = self.post(self.budget_apply_submit_url,cookies=Cookies,data=payload,files=files)
self.assertStatusCode(200)
return file_id
# # 跳过测试类
# @seldom.skip()
def test_before_apply_approval(self):
"""事前申请审批操作"""
# 第一步:添加事前申请
file_id = self.test_before_apply_submit()
# 第二步:退出申请人账号
self.logout_url = '/logout'
payload ={}
# 退出接口信息
r_userinfo = self.get(self.logout_url,data=payload)
self.assertStatusCode(200)
# 第三步:登录审批人账号
test_approval_login = LoginApprovalTest()
cookie_token =test_approval_login.test_approval_login()
Cookies = {
# 从浏览器中复制过来的Cookie
'rcm.session.id':cookie_token,
'accountSuitId':'fa80c8f8b9ee43a79fdfa5ddf8d29807'
}
# 查看审批单信息
self.before_find_apply_url = '/app/homepage/todoListDataPage'
payload = {
'pageNo':1,
'pageSize':20,
'taskName':'事前-事前申请',
'applyUserName':'',
'orderBy':5,
'applyOfficeName':'',
'applyReason':'',
'budgetName':'',
'receiverName':'',
'minApplyAmount':'',
'maxApplyAmount':''
}
# 获取待审批接口信息
r_userinfo = self.get(self.before_find_apply_url,cookies=Cookies,params=payload)
# print(r_userinfo.url)
self.assertStatusCode(200)
apply_advance = self.response['data'][0]
apply_advance_processInstanceId_app = self.response['data'][0]['task']['processInstanceId']
print(self.response)
print(apply_advance)
print(apply_advance_processInstanceId_app)
# 第四步:审批人审批
self.approval_apply_url = '/act/task/appSaveAudit'
params = {
'procInsId':apply_advance_processInstanceId_app,
'flag':'yes',
'comment':''
}
r_userinfo = self.post(self.approval_apply_url,cookies=Cookies,params=params)
self.assertStatusCode(200)
apply_advance = self.response
print(self.response)
print(apply_advance)
assert_json = {'msg': '审批成功', 'code': 0}
self.assertJSON(assert_json)
# 第五步:审批账号退出
payload ={}
# 退出接口信息
r_userinfo = self.get(self.logout_url,data=payload)
self.assertStatusCode(200)
return file_id
def test_before_apply_invalidAndReset(self):
"""事前申请单作废重置操作"""
Cookies = {
# 从浏览器中复制过来的Cookie
'rcm.session.id':self.setup(),
'accountSuitId':'fa80c8f8b9ee43a79fdfa5ddf8d29807'
}
# 第一步:添加事前申请
self.test_before_apply_submit()
# 第二步:查询提交状态的单据信息
test_select_approval = SelectApplyCommon()
apply_list = test_select_approval.select_approval_form_web(self.setup())
print(apply_list)
apply_advance_id_reset = apply_list[0]
#第三步:申请单据作废重置操作
localTime = time.strftime("%Y%m%d%H%M%S", time.localtime())
salt = ''.join(random.sample(string.ascii_letters + string.digits, 8))
rString = salt + localTime
self.before_apply_reset_url = '/budexec/beforeApply/invalidAndReset'
payload = {
'id':apply_advance_id_reset,
'invalidReason':rString
}
print(apply_advance_id_reset)
# 申请单作废接口信息
r_userinfo = self.post(self.before_apply_reset_url,cookies=Cookies,data=payload)
self.assertStatusCode(200)
assert_reset = {'code': 0}
self.assertJSON(assert_reset)
def test_before_apply_invalid(self):
"""申请单作废操作"""
Cookies = {
# 从浏览器中复制过来的Cookie
'rcm.session.id':self.setup(),
'accountSuitId':'fa80c8f8b9ee43a79fdfa5ddf8d29807'
}
# 第一步:添加事前申请
self.test_before_apply_submit()
# 第二步:查询提交状态的单据信息
test_select_approval = SelectApplyCommon()
apply_list = test_select_approval.select_approval_form_web(self.setup())
print(apply_list)
apply_advance_id_reset = apply_list[0]
#第三步:申请单据作废操作
localTime = time.strftime("%Y%m%d%H%M%S", time.localtime())
salt = ''.join(random.sample(string.ascii_letters + string.digits, 8))
rString = salt + localTime
self.before_apply_invalid_url = '/budexec/beforeApply/invalid'
payload = {
'id':apply_advance_id_reset,
'invalidReason':rString
}
print(apply_advance_id_reset)
# 申请单作废接口信息
r_userinfo = self.post(self.before_apply_invalid_url,cookies=Cookies,data=payload)
self.assertStatusCode(200)
assert_reset = {'code': 0}
self.assertJSON(assert_reset)
def test_before_apply_recall(self):
"""申请单撤回操作"""
Cookies = {
# 从浏览器中复制过来的Cookie
'rcm.session.id':self.setup(),
'accountSuitId':'fa80c8f8b9ee43a79fdfa5ddf8d29807'
}
# 第一步:添加事前申请提交
self.test_before_apply_submit()
# 第二步:查询待审批的事前申请单
select_approval_apply = SelectApplyCommon()
apply_list = select_approval_apply.select_approval_form_web(self.setup())
print(apply_list)
apply_advance_recall_taskDefKey = apply_list[1]
apply_advance_recall_activityId = apply_list[2]
apply_advance_recall_taskId = apply_list[3]
apply_advance_recall_procInsId = apply_list[4]
apply_advance_recall_procDefId = apply_list[5]
# 第三步:撤回事前申请单操作
self.before_apply_recall_url = '/act/task/ajaxRecallPro'
payload = {
'targetTaskKey':apply_advance_recall_activityId,
'taskDefKey':apply_advance_recall_taskDefKey,
'procInsId': apply_advance_recall_procInsId,
'procDefId':apply_advance_recall_procDefId,
'taskId': apply_advance_recall_taskId
}
# 申请单撤回接口信息
r_userinfo = self.post(self.before_apply_recall_url,cookies=Cookies,data=payload)
self.assertStatusCode(200)
assert_reset = {'code': 0}
self.assertJSON(assert_reset)
def test_before_apply_approval_no(self):
""""事前申请退回操作"""
# 第一步:添加事前申请
self.test_before_apply_submit()
# 第二步:退出当前账号
test_logout = LoginTest()
test_logout.logout()
# 审批账号登录
test_approval_login = LoginApprovalTest()
cookie_token =test_approval_login.test_approval_login()
Cookies = {
# 从浏览器中复制过来的Cookie
'rcm.session.id':cookie_token,
'accountSuitId':'fa80c8f8b9ee43a79fdfa5ddf8d29807'
}
# 第三步:获取app待审批列表信息
test_approval_app_list = SelectApplyCommon()
apply_list = test_approval_app_list.select_approval_form(cookie_token)
apply_advance_processInstanceId_app = apply_list
# 第四步:审批退回操作
self.approval_apply_url = '/act/task/appSaveAudit'
params = {
'procInsId':apply_advance_processInstanceId_app,
'flag':'no',
'comment':''
}
r_userinfo = self.post(self.approval_apply_url,cookies=Cookies,params=params)
self.assertStatusCode(200)
apply_advance = self.response
print(self.response)
print(apply_advance)
assert_json = {'msg': '审批成功', 'code': 0}
self.assertJSON(assert_json)
# 第五步:审批账号退出
test_logout = LoginTest()
test_logout.logout()
if __name__ == '__main__':
seldom.main()
以上是使用seldom进行部门接口自动化的示例信息,目前已完成系统60%常用模块的接口测试用例,代码部分已上传gitee上,具体使用请参考seldom版本手册。
四、总结
目前使用python框架可以很好的实现接口自动化,相比工具而言可能需要一定的时间成本,但是成效也非常显著!后期可以考虑集合jenkins完成一键部署测试,更好的实现自动化部署测试。