接口测试阶段性总结
从事软件测试工作近4年,最近一年常常感觉在原地踏步。2017的时候,曾在unittest单元测试框架基础上就部门业务特点整了一个接口测试框架(简陋版:基于python的自动化测试框架开发),采用数据驱动模式,完成测试用例编写-》测试用例执行-》测试报告自动发送的目的。可是实际推广过程并不如意,一方面组内同事会代码的并不多,一方面用例模板的表达能力欠缺(复杂的场景不好描述,主要原因),新来的同事看过去的用例常常一脸懵,而两年间部门内接口测试就换了几套方法。在现在的我理解看来接口测试最优的方法就是实现测试用例和代码的分离(貌似是老生常谈的[汗]),需要做好测试用例加载引擎和测试用例执行引擎。
关于测试用例加载引擎简单说要考虑以下几点:
1.测试用例数据结构必须包含接口测试用例完整的信息要素,URL、Headers、Method、请求正文和预期响应结果。这个好像是都知道的事情,但实际接口用例中常常并没有包含,过去我们在接口用例数据中常常关注的是请求正文和预期响应结果,URL、Headers、Method常常是写在用例执行脚本中,这么写貌似也没有问题,但是有一个不好,就是不方便新同事接手工作。在经历过各类用例描述模板折腾后,现在的我觉得用yaml描述是最好的,结构清晰。
2.不管用例采用的是什么形式描述,也不管用例是不是采用了业务分层的组织思想,我们都需要将用例描述数据统一成标准的测试用例数据,这样测试用例执行引擎才能发请求做断言。yaml用例文件是一个字典列表,标准的测试用例数据都可以通过列表和字典的方法去获取。
3.能够描述复杂接口用例。当接口某个请求参数要求是一个9位的随机数字,当响应接口为一个多层嵌套的json结构体,而你需要判断它的账号是不是符合一定的要求。这个时候仅仅文本用例是没办法描述到的。用jmeter的话,可以添加一个变量,但是yaml是标记语言,不能直接使用函数描述,但是有占位符%s替代描述,将定义这个参数的函数写在执行引擎里,然后在发请求之前调用一下就好。
4.业务场景描述。在描述一个业务场景时,常常涉及得不止一个接口,比如说通用冲账接口,测试它之前,我们必须要做一个维护类的金融交易像转账,如果我们针对每一个要测的业务逻辑,都要描述一遍要请求的接口,那么就会造成大量重复描述,用例也会变得臃肿。所以将每一个接口调用单独封装为一条测试用例,然后在描述业务测试场景时,选择对应的接口,按照顺序拼接为业务场景测试用例非常必要。
5.接口测试用例之间的传参。比如说开户之后返回的账号,要用来做转账交易,而开户是一个测试脚本,转账又是一个测试脚本,需要实现相互传参。
6.测试用例分组执行。实际测试工作中,我们可能会遇到各式各样的测试粒度需求,这个说测主要流程就好,那个说要系统测试,时间经历的不允许,这时候就要考虑如何在同一套测试脚本中提取本次测试范围内需要的用例。unittest已经有了清晰的定义,skip/skipIf/skipUnless装饰器。
YAML测试用例模板:
- #添加减号可以把用例转为list,每一部分是一个字典 name: wrong custmer and account creat request: url: http://10.22.60.42:22031/3080 method: POST headers: {'rpc_version':'1.0','rpc_group':'800','Content-Type':'application/json'} json: { "input": { "cust_no":"30086783782261", "ccy_code":"PHP", "self_opt_number_ind":"0", "prod_id":"lite", "layout_id":"1", "level_id":"lite", "customize_ind":"N", "cust_shortname":"Miki", "plan_emboss_card_date":"", "acct_no":lite_acctNo, "address":"test" }, "sys": { "country": "en_US", "prcscd": "3080" }} validators: - {"check": "status_code", "comparator": "eq", "expected": 200} - {"check": "content.sys.erorcd", "comparator": "eq", "expected": "0000"} - name: normal custmer and account creat request: url: http://10.22.60.42:22031/3080 method: POST headers: {'rpc_version':'1.0','rpc_group':'800','Content-Type':'application/json'} skip: "skip this test unconditionally" json: { "input": { "gender": "M", "combine_stmt_ind": "Y", "birth_date": "19950801", "cust_last_name_foreign": "JunJie", "push_msg_ind": "Y", "language": "en", "title": "", "birth_country": "CN", "push_sms_ind": "Y", "cust_foreign_name": "Ni JunJie", "cust_first_name_foreign": "Ni", "residence_status": "Y", "cust_no": "%s", "fund_source": "03", "cust_name": "LIANG CHENG", }], "title_thai": "001", "sign_cross_sell_ind": "Y", "cust_first_name": "LIANG", "marital_status": "002", "nationality": "CN", "cust_last_name": "CHENG" }, "sys": { "prcscd": "3080" }, "comm_req": { "device_serial_no": "", "initiator_system": "114", "trxn_branch":"1234", "trxn_teller": "88881234", "device_id": "6c5439e7-42ff-3751-a4e5-982f76201004com.sunline.Mirai", "device_model": "HWI-AL00", "call_seq": "20190304114145657165", "sponsor_system": "114", "busi_teller_id": "appuser", "mobile_no": "0147147147", "ctry_local_trans": "", "busi_org_id": "025", "device_imei": "869620039168625", "busi_seq": "20181020114145657165", "page_code": "AccountMaintenance_PIN_SetUpPin", "channel_id": "107" } } validators: - {"check": "status_code", "comparator": "eq", "expected": 200} - {"check": "content.sys.erorcd", "comparator": "eq", "expected": "0000"} - name: normal custmer and account creat request: url: http://10.22.60.42:22031/3080 method: POST headers: {'rpc_version':'1.0','rpc_group':'800','Content-Type':'application/json'} json: { "input": { "gender": "M", "combine_stmt_ind": "Y", "birth_date": "19950801", "cust_last_name_foreign": "JunJie", "push_msg_ind": "Y", "language": "en", "title": "", "birth_country": "CN", "push_sms_ind": "Y", "cust_foreign_name": "Ni JunJie", "cust_first_name_foreign": "Ni", "residence_status": "Y", "list01": [{ "doc_status": "1", "doc_no": "%s", "doc_expy_date": "20991231", "doc_type": "001", "doc_effe_date": "20000101", "main_info_ind": "Y" }, { "doc_status": "1", "doc_no": "EC0783791", "doc_expy_date": "20280228", "doc_type": "002", "doc_effe_date": "20180301", "main_info_ind": "N", "doc_issuing_country": "CN" }], "cust_no": "%s", "nationality": "CN", "cust_last_name": "CHENG" }, "sys": { "prcscd": "3080" }, "comm_req": { "device_serial_no": "", "initiator_system": "114", "trxn_branch":"1234", "trxn_teller": "88881234", "device_id": "6c5439e7-42ff-3751-a4e5-982f76201004com.sunline.Mirai", "device_model": "HWI-AL00", "call_seq": "20190304114145657165", "sponsor_system": "114", "channel_id": "107" } } validators: - {"check": "status_code", "comparator": "eq", "expected": 200} - {"check": "content.sys.erorcd", "comparator": "eq", "expected": "0000"}
ps:测试用例分组执行,在YAML
测试用例中,新增skip/skipIf/skipUnless
参数,然后在接口测试脚本中根据参数内容来决定是否执行raise SkipTest(reason)
接口用例测试脚本模板:
#coding=utf-8 import yaml import random import requests import json class SkipTest(Exception): """ Raise this exception in a test to skip it. Usually you can use TestCase.skipTest() or one of the skipping decorators instead of raising this directly. """ pass def fmat(k): seq = ''.join([str(i) for i in random.sample(range(0,9),k)]) return seq custNo = '100'+ fmat(9) doc_no = fmat(9) f = open(r"C:\Users\admin\Desktop\ryana\3080.yaml") testcase_list = yaml.load(f) #print testcase_list #对需要参数化的字段重新赋值 Oldcust_no = testcase_list[1]['request']['json']['input']['cust_no'] print '--------------------hi--------------',Oldcust_no #输出为%,%为占位符 Oldcust_no = Oldcust_no%custNo testcase_list[1]['request']['json']['input']['cust_no'] = Oldcust_no print '--------------------hi--------------',Oldcust_no #输出为更新后的cust_no Olddoc_no = testcase_list[1]['request']['json']['input']['list01'][0]['doc_no'] Olddoc_no = Olddoc_no%doc_no testcase_list[1]['request']['json']['input']['list01'][0]['doc_no'] = Olddoc_no seq_3080 = [] global seq_3080 for i in range(len(testcase_list)): print u"开始执行第%s个用例--------"%(i+1) #检查该用例是否需要执行 if "skip" in testcase_list[i]['request']: try: skip_reason = testcase_list[i]['request']["skip"] print skip_reason except: raise SkipTest(skip_reason) continue req = testcase_list[i]['request']['json'] url = testcase_list[i]['request']['url'] headers = testcase_list[i]['request']['headers'] r0 = requests.post(url = url,json = req,headers = headers) print r0.status_code d0 = json.loads(r0.content) #判断该用例是否执行通过 erorcd = d0['sys']['erorcd'] #print erorcd if erorcd == testcase_list[i]['validators'][1]['check']: print '3080 testcase pass' else: print '3080 testcase fail' #收集交易流水 seq_3080.append(d0['sys']['trxn_seq'].encode()) print u'3080交易流水表:',seq_3080
业务场景用例脚本模板:
#coding=utf-8 import yaml import requests import json from test3080 import seq_3080 class SkipTest(Exception): """ Raise this exception in a test to skip it. Usually you can use TestCase.skipTest() or one of the skipping decorators instead of raising this directly. """ pass f = open(r"C:\Users\admin\Desktop\ryana\1000.yaml") testcase_list = yaml.load(f) #print testcase_list seq_1000 = [] global seq_1000 for i in range(len(testcase_list)): print u"开始执行第%s个用例--------"%(i+1) #检查该用例是否需要执行 if "skip" in testcase_list[i]['request']: try: skip_reason = testcase_list[i]['request']["skip"] print skip_reason except: raise SkipTest(skip_reason) continue Oldseq = testcase_list[0]['request']['json']['input']['orig_initiator_seq'] #引用全局变量 Oldseq = Oldseq%seq_3080[i] testcase_list[0]['request']['json']['input']['orig_initiator_seq'] = Oldseq.encode() req = testcase_list[i]['request']['json'] url = testcase_list[i]['request']['url'] headers = testcase_list[i]['request']['headers'] r0 = requests.post(url = url,json = req,headers = headers) print r0.status_code d0 = json.loads(r0.content) #判断该用例是否执行通过 erorcd = d0['sys']['erorcd'] #print erorcd if erorcd == testcase_list[i]['validators'][1]['check']: print '1000 testcase pass' else: print '1000 testcase fail' #收集交易流水 seq_1000.append(d0['sys']['trxn_seq'].encode()) print u'1000交易流水表:',seq_1000
总结:针对某个用例中的某个字段参数化,上述脚本表达的还是不够清晰,在我看来加不加多线程,要不要for循环其实作用并不大的,它们更像是锦上添花的功能,所以更愿意将关注放在用例数据上,它们是不是很好的描述业务场景,是不是很好的覆盖业务场景,这才是最关键的。