一、爲何要寫代碼實現接口自動化html
你們知道不少接口測試工具能夠實現對接口的測試,如postman、jmeter、fiddler等等,並且使用方便,那麼爲何還要寫代碼實現接口自動化呢?工具雖然方便,但也不足之處:前端
測試數據不可控制python
接口測試本質是對數據的測試,調用接口,輸入一些數據,隨後,接口返回一些數據。驗證接口返回數據的正確性。在用工具運行測試用例以前不得不手動向數據庫中插入測試數據。這樣咱們的接口測試是否是就沒有那麼「自動化了」。mysql
沒法測試加密接口sql
這是接口測試工具的一大硬傷,如咱們前面開發的接口用工具測試徹底沒有問題,但遇到須要對接口參 數進行加密/解密的接口,例如 md五、base6四、AES 等常見加密方式。本書第十一章會對加密接口進行介紹。 又或者接口的參數須要使用時間戳,也是工具很難模擬的。數據庫
擴展能力不足django
當咱們在享受工具所帶來的便利的同時,每每也會受制於工具所帶來的侷限。例如,我想將測試結果生 成 HMTL 格式測試報告,我想將測試報告發送到指定郵箱。我想對接口測試作定時任務。我想對接口測試作持續集成。這些需求都是工具難以實現的。json
二、接口自動化測試設計後端
接口測試調用過程能夠用下圖歸納,增長了測試數據庫api
通常的接口工具測試過程:
一、接口工具調用被測系統的接口(傳參 username="zhangsan")。
二、系統接口根據傳參(username="zhangsan")向正式數據庫中查詢數據。
三、將查詢結果組裝成必定格式的數據,並返回給被調用者。
四、人工或經過工具的斷言功能檢查接口測試的正確性。
接口自動化測試項目,爲了使接口測試對數據變得可控,測試過程以下:
一、接口測試項目先向測試數據庫中插入測試數據(zhangsan 的我的信息)。
二、調用被測系統接口(傳參 username="zhangsan")。
三、系統接口根據傳參(username="zhangsan")向測試數據庫中進行查詢並獲得 zhangsan 我的信息。
四、將查詢結果組裝成必定格式的數據,並返回給被調用者。
五、經過單元測試框架斷言接口返回的數據(zhangsan 的我的信息),並生成測試報告。
爲了使正式數據庫的數據不被污染,建議使用獨立的測試數據庫。
二、requests庫
Requests 使用的是 urllib3,所以繼承了它的全部特性。Requests 支持 HTTP 鏈接保持和鏈接池,支持使用cookie保持會話,支持文件上傳,支持自動肯定響應內容的編碼。對request庫的更詳細的介紹能夠看我以前接口測試基礎的博客:
http://www.cnblogs.com/ailiailan/p/7388784.html
http://www.cnblogs.com/ailiailan/p/7412945.html
三、接口測試代碼示例
下面以以前用python+django開發的用戶簽到系統爲背景,展現接口測試的代碼。
爲何開發接口?開發的接口主要給誰來用?
前端和後端分離是近年來 Web 應用開發的一個發展趨勢。這種模式將帶來如下優點:
一、後端能夠不用必須精通前端技術(HTML/JavaScript/CSS),只專一於數據的處理,對外提供 API 接口。
二、前端的專業性愈來愈高,經過 API 接口獲取數據,從而專一於頁面的設計。
三、先後端分離增長接口的應用範圍,開發的接口能夠應用到 Web 頁面上,也能夠應用到移動 APP 上。
在這種開發模式下,接口測試工做就會變得尤其重要了。
開發實現的接口代碼示例:
1 # 添加發佈會接口實現 2 def add_event(request): 3 eid = request.POST.get('eid','') # 發佈會id 4 name = request.POST.get('name','') # 發佈會標題 5 limit = request.POST.get('limit','') # 限制人數 6 status = request.POST.get('status','') # 狀態 7 address = request.POST.get('address','') # 地址 8 start_time = request.POST.get('start_time','') # 發佈會時間 9 10 if eid =='' or name == '' or limit == '' or address == '' or start_time == '': 11 return JsonResponse({'status':10021,'message':'parameter error'}) 12 13 result = Event.objects.filter(id=eid) 14 if result: 15 return JsonResponse({'status':10022,'message':'event id already exists'}) 16 17 result = Event.objects.filter(name=name) 18 if result: 19 return JsonResponse({'status':10023,'message':'event name already exists'}) 20 21 if status == '': 22 status = 1 23 24 try: 25 Event.objects.create(id=eid,name=name,limit=limit,address=address,status=int(status),start_time=start_time) 26 except ValidationError: 27 error = 'start_time format error. It must be in YYYY-MM-DD HH:MM:SS format.' 28 return JsonResponse({'status':10024,'message':error}) 29 30 return JsonResponse({'status':200,'message':'add event success'})
經過POST請求接收發佈會參數:發佈會id、標題、人數、狀態、地址和時間等參數。
首先,判斷eid、name、limit、address、start_time等字段均不能爲空,不然JsonResponse()返回相應的狀態碼和提示。JsonResponse()是一個很是有用的方法,它能夠直接將字典轉化成Json格式返回到客戶端。
接下來,判斷髮佈會id是否存在,以及發佈會名稱(name)是否存在;若是存在將返回相應的狀態碼和 提示信息。
再接下來,判斷髮佈會狀態是否爲空,若是爲空,將狀態設置爲1(True)。
最後,將數據插入到 Event 表,在插入的過程當中若是日期格式錯誤,將拋出 ValidationError 異常,接收 該異常並返回相應的狀態和提示,不然,插入成功,返回狀態碼200和「add event success」的提示。
1 # 發佈會查詢接口實現 2 def get_event_list(request): 3 4 eid = request.GET.get("eid", "") # 發佈會id 5 name = request.GET.get("name", "") # 發佈會名稱 6 7 if eid == '' and name == '': 8 return JsonResponse({'status':10021,'message':'parameter error'}) 9 10 if eid != '': 11 event = {} 12 try: 13 result = Event.objects.get(id=eid) 14 except ObjectDoesNotExist: 15 return JsonResponse({'status':10022, 'message':'query result is empty'}) 16 else: 17 event['eid'] = result.id 18 event['name'] = result.name 19 event['limit'] = result.limit 20 event['status'] = result.status 21 event['address'] = result.address 22 event['start_time'] = result.start_time 23 return JsonResponse({'status':200, 'message':'success', 'data':event}) 24 25 if name != '': 26 datas = [] 27 results = Event.objects.filter(name__contains=name) 28 if results: 29 for r in results: 30 event = {} 31 event['eid'] = r.id 32 event['name'] = r.name 33 event['limit'] = r.limit 34 event['status'] = r.status 35 event['address'] = r.address 36 event['start_time'] = r.start_time 37 datas.append(event) 38 return JsonResponse({'status':200, 'message':'success', 'data':datas}) 39 else: 40 return JsonResponse({'status':10022, 'message':'query result is empty'})
經過GET請求接收發佈會id和name 參數。兩個參數都是可選的。首先,判斷當兩個參數同時爲空,接口返回狀態碼10021,參數錯誤。
若是發佈會id不爲空,優先經過id查詢,由於id的惟一性,因此,查詢結果只會有一條,將查詢結果 以 key:value 對的方式存放到定義的event字典中,並將數據字典做爲整個返回字典中data對應的值返回。
name查詢爲模糊查詢,查詢數據可能會有多條,返回的數據稍顯複雜;首先將查詢的每一條數據放到一 個字典event中,再把每個字典再放到數組datas中,最後再將整個數組作爲返回字典中data對應的值返回。
接口測試代碼示例:
1 #查詢發佈會接口測試代碼 2 import requests 3 4 url = "http://127.0.0.1:8000/api/get_event_list/" 5 r = requests.get(url, params={'eid':'1'}) 6 result = r.json() 7 print(result) 8 assert result['status'] == 200 9 assert result['message'] == "success" 10 assert result['data']['name'] == "xx 產品發佈會" 11 assert result['data']['address'] == "北京林匹克公園水立方" 12 assert result['data']['start_time'] == "2016-10-15T18:00:00"
由於「發佈會查詢接口」是GET類型,因此,經過requests庫的get()方法調用,第一個參數爲調用接口的URL地址,params設置接口的參數,參數以字典形式組織。
json()方法能夠將接口返回的json格式的數據轉化爲字典。
接下來就是經過 assert 語句對接字典中的數據進行斷言。分別斷言status、message 和data的相關數據等。
使用unittest單元測試框架開發接口測試用例
1 #發佈會查詢接口測試代碼
2 import unittest
3 import requests 4 5 class GetEventListTest(unittest.TestCase): 6 7 def setUp(self): 8 self.base_url = "http://127.0.0.1:8000/api/get_event_list/" 9 10 def test_get_event_list_eid_null(self): 11 ''' eid 參數爲空 ''' 12 r = requests.get(self.base_url, params={'eid':''}) 13 result = r.json() 14 self.assertEqual(result['status'], 10021) 15 self.assertEqual(result['message'], 'parameter error') 16 17 def test_get_event_list_eid_error(self): 18 ''' eid=901 查詢結果爲空 ''' 19 r = requests.get(self.base_url, params={'eid':901}) 20 result = r.json() 21 self.assertEqual(result['status'], 10022) 22 self.assertEqual(result['message'], 'query result is empty') 23 24 def test_get_event_list_eid_success(self): 25 ''' 根據 eid 查詢結果成功 ''' 26 r = requests.get(self.base_url, params={'eid':1}) 27 result = r.json() 28 self.assertEqual(result['status'], 200) 29 self.assertEqual(result['message'], 'success') 30 self.assertEqual(result['data']['name'],u'mx6發佈會') 31 self.assertEqual(result['data']['address'],u'北京國家會議中心') 32 33 def test_get_event_list_nam_result_null(self): 34 ''' 關鍵字‘abc’查詢 ''' 35 r = requests.get(self.base_url, params={'name':'abc'}) 36 result = r.json() 37 self.assertEqual(result['status'], 10022) 38 self.assertEqual(result['message'], 'query result is empty') 39 40 def test_get_event_list_name_find(self): 41 ''' 關鍵字‘發佈會’模糊查詢 ''' 42 r = requests.get(self.base_url, params={'name':'發佈會'}) 43 result = r.json() 44 self.assertEqual(result['status'], 200) 45 self.assertEqual(result['message'], 'success') 46 self.assertEqual(result['data'][0]['name'],u'mx6發佈會') 47 self.assertEqual(result['data'][0]['address'],u'北京國家會議中心')
48
49if __name__ == '__main__':
50 unittest.main()
unittest單元測試框架能夠幫助組織和運行接口測試用例。
四、接口自動化測試框架實現
關於接口自動化測試,unittest 已經幫咱們作了大部分工做,接下來只須要集成數據庫操做,以及HTMLTestRunner測試報告生成擴展便可。
框架結構以下圖:
pyrequests 框架:
db_fixture/: 初始化接口測試數據。
interface/: 用於編寫接口自動化測試用例。
report/: 生成接口自動化測試報告。
db_config.ini : 數據庫配置文件。
HTMLTestRunner.py unittest 單元測試框架擴展,生成 HTML 格式的測試報告。
run_tests.py : 執行全部接口測試用例。
4.一、數據庫配置
首先,須要修改被測系統將數據庫指向測試數據庫。以 MySQL數據庫爲例,針對django項目而言,修改.../guest/settings.py 文件。能夠在系統測試環境單首創建一個測試庫。這樣作的目的是讓接口測試的數據不會清空或污染到功能測試庫的數據。其餘框架開發的項目與django項目相似,這個工做通常由開發同窗完成,咱們測試同窗更多關注的是測試框架的代碼。
4.二、框架代碼實現
4.2.1、首先,創建數據庫配置文件.../db_config.ini
4.2.2、接下來,簡單封裝數據庫操做,數據庫表數據的插入和清除,.../db_fixture/mysql_db.py
1 import pymysql.cursors 2 import os 3 import configparser as cparser 4 5 6 # ======== Reading db_config.ini setting =========== 7 base_dir = str(os.path.dirname(os.path.dirname(__file__))) 8 base_dir = base_dir.replace('\\', '/') 9 file_path = base_dir + "/db_config.ini" 10 11 cf = cparser.ConfigParser() 12 13 cf.read(file_path) 14 host = cf.get("mysqlconf", "host") 15 port = cf.get("mysqlconf", "port") 16 db = cf.get("mysqlconf", "db_name") 17 user = cf.get("mysqlconf", "user") 18 password = cf.get("mysqlconf", "password") 19 20 21 # ======== MySql base operating =================== 22 class DB: 23 24 def __init__(self): 25 try: 26 # Connect to the database 27 self.connection = pymysql.connect(host=host, 28 port=int(port), 29 user=user, 30 password=password, 31 db=db, 32 charset='utf8mb4', 33 cursorclass=pymysql.cursors.DictCursor) 34 except pymysql.err.OperationalError as e: 35 print("Mysql Error %d: %s" % (e.args[0], e.args[1])) 36 37 # clear table data 38 def clear(self, table_name): 39 # real_sql = "truncate table " + table_name + ";" 40 real_sql = "delete from " + table_name + ";" 41 with self.connection.cursor() as cursor: 42 cursor.execute("SET FOREIGN_KEY_CHECKS=0;") 43 cursor.execute(real_sql) 44 self.connection.commit() 45 46 # insert sql statement 47 def insert(self, table_name, table_data): 48 for key in table_data: 49 table_data[key] = "'"+str(table_data[key])+"'" 50 key = ','.join(table_data.keys()) 51 value = ','.join(table_data.values()) 52 real_sql = "INSERT INTO " + table_name + " (" + key + ") VALUES (" + value + ")" 53 #print(real_sql) 54 55 with self.connection.cursor() as cursor: 56 cursor.execute(real_sql) 57 58 self.connection.commit() 59 60 # close database 61 def close(self): 62 self.connection.close() 63 64 # init data 65 def init_data(self, datas): 66 for table, data in datas.items(): 67 self.clear(table) 68 for d in data: 69 self.insert(table, d) 70 self.close() 71 72 73 if __name__ == '__main__': 74 75 db = DB() 76 table_name = "sign_event" 77 data = {'id':1,'name':'紅米','`limit`':2000,'status':1,'address':'北京會展中心','start_time':'2016-08-20 00:25:42'} 78 table_name2 = "sign_guest" 79 data2 = {'realname':'alen','phone':12312341234,'email':'alen@mail.com','sign':0,'event_id':1} 80 81 db.clear(table_name) 82 db.insert(table_name, data) 83 db.close()
首先,讀取 db_config.ini 配置文件。 建立 DB 類,__init__()方法初始化,經過 pymysql.connect()鏈接數據庫。
由於這裏只用到數據庫表的清除和插入,因此只建立 clear()和 insert()兩個方法。其中,insert()方法對數 據的插入作了簡單的格式轉化,可將字典轉化成 SQL 插入語句,這樣格式轉化了方便了數據庫表數據的建立。
最後,經過 close()方法用於關閉數據庫鏈接。
4.2.3、接下來接下來建立測試數據,.../db_fixture/test_data.py
1 import sys 2 sys.path.append('../db_fixture') 3 try: 4 from mysql_db import DB 5 except ImportError: 6 from .mysql_db import DB 7 8 # create data 9 datas = { 10 'sign_event':[ 11 {'id':1,'name':'紅米Pro發佈會','`limit`':2000,'status':1,'address':'北京會展中心','start_time':'2017-08-20 14:00:00'}, 12 {'id':2,'name':'可參加人數爲0','`limit`':0,'status':1,'address':'北京會展中心','start_time':'2017-08-20 14:00:00'}, 13 {'id':3,'name':'當前狀態爲0關閉','`limit`':2000,'status':0,'address':'北京會展中心','start_time':'2017-08-20 14:00:00'}, 14 {'id':4,'name':'發佈會已結束','`limit`':2000,'status':1,'address':'北京會展中心','start_time':'2001-08-20 14:00:00'}, 15 {'id':5,'name':'小米5發佈會','`limit`':2000,'status':1,'address':'北京國家會議中心','start_time':'2017-08-20 14:00:00'}, 16 ], 17 'sign_guest':[ 18 {'id':1,'realname':'alen','phone':13511001100,'email':'alen@mail.com','sign':0,'event_id':1}, 19 {'id':2,'realname':'has sign','phone':13511001101,'email':'sign@mail.com','sign':1,'event_id':1}, 20 {'id':3,'realname':'tom','phone':13511001102,'email':'tom@mail.com','sign':0,'event_id':5}, 21 ], 22 } 23 24 # Inster table datas 25 def init_data(): 26 DB().init_data(datas) 27 28 29 if __name__ == '__main__': 30 init_data()
init_data()函數用於讀取 datas 字典中的數據,調用 DB 類中的 clear()方法清除數據庫,而後,調用 insert() 方法插入表數據。
4.2.4、編寫接口測試用例。建立添加發佈會接口測試文件.../interface/add_event_test.py
1 import unittest 2 import requests 3 import os, sys 4 parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 sys.path.insert(0, parentdir) 6 from db_fixture import test_data 7 8 9 class AddEventTest(unittest.TestCase): 10 ''' 添加發佈會 ''' 11 12 def setUp(self): 13 self.base_url = "http://127.0.0.1:8000/api/add_event/" 14 15 def tearDown(self): 16 print(self.result) 17 18 def test_add_event_all_null(self): 19 ''' 全部參數爲空 ''' 20 payload = {'eid':'','':'','limit':'','address':"",'start_time':''} 21 r = requests.post(self.base_url, data=payload) 22 self.result = r.json() 23 self.assertEqual(self.result['status'], 10021) 24 self.assertEqual(self.result['message'], 'parameter error') 25 26 def test_add_event_eid_exist(self): 27 ''' id已經存在 ''' 28 payload = {'eid':1,'name':'一加4發佈會','limit':2000,'address':"深圳寶體",'start_time':'2017'} 29 r = requests.post(self.base_url, data=payload) 30 self.result = r.json() 31 self.assertEqual(self.result['status'], 10022) 32 self.assertEqual(self.result['message'], 'event id already exists') 33 34 def test_add_event_name_exist(self): 35 ''' 名稱已經存在 ''' 36 payload = {'eid':11,'name':'紅米Pro發佈會','limit':2000,'address':"深圳寶體",'start_time':'2017'} 37 r = requests.post(self.base_url,data=payload) 38 self.result = r.json() 39 self.assertEqual(self.result['status'], 10023) 40 self.assertEqual(self.result['message'], 'event name already exists') 41 42 def test_add_event_data_type_error(self): 43 ''' 日期格式錯誤 ''' 44 payload = {'eid':11,'name':'一加4手機發佈會','limit':2000,'address':"深圳寶體",'start_time':'2017'} 45 r = requests.post(self.base_url,data=payload) 46 self.result = r.json() 47 self.assertEqual(self.result['status'], 10024) 48 self.assertIn('start_time format error.', self.result['message']) 49 50 def test_add_event_success(self): 51 ''' 添加成功 ''' 52 payload = {'eid':11,'name':'一加4手機發佈會','limit':2000,'address':"深圳寶體",'start_time':'2017-05-10 12:00:00'} 53 r = requests.post(self.base_url,data=payload) 54 self.result = r.json() 55 self.assertEqual(self.result['status'], 200) 56 self.assertEqual(self.result['message'], 'add event success') 57 58 59 if __name__ == '__main__': 60 test_data.init_data() # 初始化接口測試數據 61 unittest.main()
在測試接口以前,調用test_data.py文件中的init_data()方法初始化數據庫中的測試數據。
建立AddEventTest測試類繼承 unittest.TestCase 類,經過建立測試用例,調用相關接口,並驗證接口返回 的數據。
4.2.5、建立run_tests.py文件
當開發的接口達到必定數量後,就須要考慮分文件分目錄的來劃分接口測試用例,如何批量的執行不一樣文件目錄下的用例呢?unittest單元測試框架提供的discover()方法能夠幫助咱們作到這一點。並使用 HTMLTestRunner 擴展生成 HTML 格式的測試報告。
1 import time, sys 2 sys.path.append('./interface') 3 sys.path.append('./db_fixture') 4 from HTMLTestRunner import HTMLTestRunner 5 import unittest 6 from db_fixture import test_data 7 8 9 # 指定測試用例爲當前文件夾下的 interface 目錄 10 test_dir = './interface' 11 discover = unittest.defaultTestLoader.discover(test_dir, pattern='*_test.py') 12 13 14 if __name__ == "__main__": 15 test_data.init_data() # 初始化接口測試數據 16 17 now = time.strftime("%Y-%m-%d %H_%M_%S") 18 filename = './report/' + now + '_result.html' 19 fp = open(filename, 'wb') 20 runner = HTMLTestRunner(stream=fp, 21 title='Guest Manage System Interface Test Report', 22 description='Implementation Example with: ') 23 runner.run(discover) 24 fp.close()
首先,經過調用test_data.py文件中的init_data()函數來初始化接口測試數據。
使用unittest框架所提供的discover()方法,查找 interface/ 目錄下,全部匹配*_test.py 的測試文件(*星 號匹配任意字符)。
HTMLTestRunner 爲unittest單元測試框架的擴展,利用它所提供的HTMLTestRunner()類來替換unittest單元測試框架的TextTestRunner()類,從而生成HTML格式的測試報告。
遺憾的是HTMLTestRunner並不支持Python3.x,你們能夠在網上找到適用於Python3.x的HTMLTestRunner.py文件,使用在本身的接口自動化工程中。
經過 time 的 strftime()方法獲取當前時間,而且轉化成必定的時間格式。做爲測試報告的名稱。這樣作目的是是爲了不由於生成的報告的名稱重名而形成報告的覆蓋。最終,將測試報告存放於report/目錄下面。以下圖,一張完整的接口自動化測試報告。