從事軟件測試工作近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循環其實作用並不大的,它們更像是錦上添花的功能,所以更願意將關注放在用例數據上,它們是不是很好的描述業務場景,是不是很好的覆蓋業務場景,這才是最關鍵的。
