Python+request+unittest實現介面測試框架組成執行個體,pythonunittest
1、為什麼要寫代碼實現介面自動化
大家知道很多介面測試載入器可以實現對介面的測試,如postman、jmeter、fiddler等等,而且使用方便,那麼為什麼還要寫代碼實現介面自動化呢?工具雖然方便,但也不足之處:
測試資料不可控制
介面測試本質是對資料的測試,調用介面,輸入一些資料,隨後,介面返回一些資料。驗證介面返回資料的正確性。在用工具運行測試案例之前不得不手動向資料庫中插入測試資料。這樣我們的介面測試是不是就沒有那麼“自動化了”。
無法測試加密介面
這是介面測試載入器的一大硬傷,如我們前面開發的介面用工具測試完全沒有問題,但遇到需要對介面參 數進行加密/解密的介面,例如 md5、base64、AES 等常見加密方式。本書第十一章會對加密介面進行介紹。 又或者介面的參數需要使用時間戳,也是工具很難類比的。
擴充能力不足
當我們在享受工具所帶來的便利的同時,往往也會受制於工具所帶來的局限。例如,我想將測試結果生 成 HMTL 格式測試報告,我想將測試報告發送到指定郵箱。我想對介面測試做定時任務。我想對介面測試做持續整合。這些需求都是工具難以實現的。
2、介面自動化測試設計
介面測試調用過程可以用概括,增加了測試資料庫
一般的 介面工具 測試過程:
1、介面工具調用被測系統的介面(傳參 username="zhangsan")。
2、系統介面根據傳參(username="zhangsan")向 正式資料庫 中查詢資料。
3、將查詢結果組裝成一定格式的資料,並返回給被調用者。
4、人工或通過工具的斷言功能檢查介面測試的正確性。
介面自動化測試專案,為了使介面測試對資料變得可控,測試過程如下:
1、介面測試專案先向 測試資料庫 中插入測試資料(zhangsan 的個人資訊)。
2、調用被測系統介面(傳參 username="zhangsan")。
3、系統介面根據傳參(username="zhangsan")向測試資料庫中進行查詢並得到 zhangsan 個人資訊。
4、將查詢結果組裝成一定格式的資料,並返回給被調用者。
5、通過單元測試架構斷言介面返回的資料(zhangsan 的個人資訊),並產生測試報告。
為了使正式資料庫的資料不被汙染,建議使用獨立的 測試資料庫 。
2、requests庫
Requests 使用的是 urllib3,因此繼承了它的所有特性。Requests 支援 HTTP 串連保持和串連池 ,支援 使用cookie保持會話 ,支援 檔案上傳 ,支援 自動確定響應內容的編碼。 對request庫的更詳細的介紹可以看我之前介面測試基礎的文章:
http://www.bkjia.com/article/122571.htm?pc
http://www.bkjia.com/article/108168.htm
3、介面測試程式碼範例
下面以之前用 python+django 開發的使用者簽到系統為背景,展示介面測試的代碼。
為什麼開發介面?開發的介面主要給誰來用?
前端和後端分離是近年來 Web 應用程式開發的一個發展趨勢。這種模式將帶來以下優勢:
1、後端可以不用必須精通前端技術(HTML/JavaScript/CSS),只專註於資料的處理,對外提供 API 介面。
2、前端的專業性越來越高,通過 API 介面擷取資料,從而專註於頁面的設計。
3、前後端分離增加介面的應用範圍,開發的介面可以應用到 Web 頁面上,也可以應用到移動 APP 上。
在這種開發模式下,介面測試工作就會變得尤為重要了。
開發實現的介面程式碼範例:
# 添加發布會介面實現def add_event(request): eid = request.POST.get('eid','') # 發布會id name = request.POST.get('name','') # 發布會標題 limit = request.POST.get('limit','') # 限制人數 status = request.POST.get('status','') # 狀態 address = request.POST.get('address','') # 地址 start_time = request.POST.get('start_time','') # 發布會時間 if eid =='' or name == '' or limit == '' or address == '' or start_time == '': return JsonResponse({'status':10021,'message':'parameter error'}) result = Event.objects.filter(id=eid) if result: return JsonResponse({'status':10022,'message':'event id already exists'}) result = Event.objects.filter(name=name) if result: return JsonResponse({'status':10023,'message':'event name already exists'}) if status == '': status = 1 try: Event.objects.create(id=eid,name=name,limit=limit,address=address,status=int(status),start_time=start_time) except ValidationError: error = 'start_time format error. It must be in YYYY-MM-DD HH:MM:SS format.' return JsonResponse({'status':10024,'message':error}) 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”的提示。
# 發布會查詢介面實現def get_event_list(request): eid = request.GET.get("eid", "") # 發布會id name = request.GET.get("name", "") # 發布會名稱 if eid == '' and name == '': return JsonResponse({'status':10021,'message':'parameter error'}) if eid != '': event = {} try: result = Event.objects.get(id=eid) except ObjectDoesNotExist: return JsonResponse({'status':10022, 'message':'query result is empty'}) else: event['eid'] = result.id event['name'] = result.name event['limit'] = result.limit event['status'] = result.status event['address'] = result.address event['start_time'] = result.start_time return JsonResponse({'status':200, 'message':'success', 'data':event}) if name != '': datas = [] results = Event.objects.filter(name__contains=name) if results: for r in results: event = {} event['eid'] = r.id event['name'] = r.name event['limit'] = r.limit event['status'] = r.status event['address'] = r.address event['start_time'] = r.start_time datas.append(event) return JsonResponse({'status':200, 'message':'success', 'data':datas}) else: return JsonResponse({'status':10022, 'message':'query result is empty'})
通過GET請求接收發布會id和name 參數。兩個參數都是可選的。首先,判斷當兩個參數同時為空白,介面返回狀態代碼10021,參數錯誤。
如果發布會id不為空白,優先通過id查詢,因為id的唯一性,所以,查詢結果只會有一條,將查詢結果 以 key:value 對的方式存放到定義的event字典中,並將資料字典作為整個返回字典中data對應的值返回。
name查詢為模糊查詢,查詢資料可能會有多條,返回的資料稍顯複雜;首先將查詢的每一條資料放到一 個字典event中,再把每一個字典再放到數組datas中,最後再將整個數組做為返回字典中data對應的值返回。
介面測試程式碼範例
#查詢發布會介面測試代碼import requestsurl = "http://127.0.0.1:8000/api/get_event_list/"r = requests.get(url, params={'eid':'1'})result = r.json()print(result)assert result['status'] == 200assert result['message'] == "success"assert result['data']['name'] == "xx 產品發布會"assert result['data']['address'] == "北京林匹克公園水立方"assert result['data']['start_time'] == "2016-10-15T18:00:00"
因為“發布會查詢介面”是GET類型,所以,通過requests庫的get()方法調用,第一個參數為調用介面的URL地址,params設定介面的參數,參數以字典形式組織。
json()方法可以將介面返回的json格式的資料轉化為字典。
接下來就是通過 assert 語句對接字典中的資料進行斷言。分別斷言status、message 和data的相關資料等。
使用unittest單元測試架構開發介面測試案例
#發布會查詢介面測試代碼 import unittestimport requestsclass GetEventListTest(unittest.TestCase): def setUp(self): self.base_url = "http://127.0.0.1:8000/api/get_event_list/" def test_get_event_list_eid_null(self): ''' eid 參數為空白 ''' r = requests.get(self.base_url, params={'eid':''}) result = r.json() self.assertEqual(result['status'], 10021) self.assertEqual(result['message'], 'parameter error') def test_get_event_list_eid_error(self): ''' eid=901 查詢結果為空白 ''' r = requests.get(self.base_url, params={'eid':901}) result = r.json() self.assertEqual(result['status'], 10022) self.assertEqual(result['message'], 'query result is empty') def test_get_event_list_eid_success(self): ''' 根據 eid 查詢結果成功 ''' r = requests.get(self.base_url, params={'eid':1}) result = r.json() self.assertEqual(result['status'], 200) self.assertEqual(result['message'], 'success') self.assertEqual(result['data']['name'],u'mx6發布會') self.assertEqual(result['data']['address'],u'北京國家會議中心') def test_get_event_list_nam_result_null(self): ''' 關鍵字‘abc'查詢 ''' r = requests.get(self.base_url, params={'name':'abc'}) result = r.json() self.assertEqual(result['status'], 10022) self.assertEqual(result['message'], 'query result is empty') def test_get_event_list_name_find(self): ''' 關鍵字‘發布會'模糊查詢 ''' r = requests.get(self.base_url, params={'name':'發布會'}) result = r.json() self.assertEqual(result['status'], 200) self.assertEqual(result['message'], 'success') self.assertEqual(result['data'][0]['name'],u'mx6發布會') self.assertEqual(result['data'][0]['address'],u'北京國家會議中心')49if __name__ == '__main__': unittest.main()
unittest單元測試架構可以協助 組織和運行介面測試案例。
4、介面自動化測試架構實現
關於介面自動化測試,unittest 已經幫我們做了大部分工作,接下來只需要 整合資料庫操作 ,以及 HTMLTestRunner測試報告產生 擴充即可。
架構結構如:
pyrequests 架構:
db_fixture/: 初始化介面測試資料。
interface/: 用於編寫介面自動化測試案例。
report/: 產生介面自動化測試報告。
db_config.ini : 資料庫設定檔。
HTMLTestRunner.py unittest 單元測試架構擴充,產生 HTML 格式的測試報告。
run_tests.py : 執行所有介面測試案例。
4.1、資料庫配置
首先,需要修改被測系統將資料庫指向測試資料庫。以 MySQL資料庫為例,針對 django 項目而言,修改.../guest/settings.py 檔案。可以在系統測試環境單獨建立一個測試庫。 這樣做的目的是讓介面測試的資料不會清空或汙染到功能測試庫的資料。 其他架構開發的項目與django項目類似,這個工作一般由開發同學完成,我們測試同學更多關注的是測試架構的代碼。
4.2、架構代碼實現
4.2.1、首先,創 建資料庫設定檔.../db_config.ini
4.2.2、接下來, 簡單封裝資料庫操作,資料庫表資料的插入和清除 ,.../db_fixture/ mysql_db.py
import pymysql.cursorsimport osimport configparser as cparser# ======== Reading db_config.ini setting ===========base_dir = str(os.path.dirname(os.path.dirname(__file__)))base_dir = base_dir.replace('\\', '/')file_path = base_dir + "/db_config.ini"cf = cparser.ConfigParser()cf.read(file_path)host = cf.get("mysqlconf", "host")port = cf.get("mysqlconf", "port")db = cf.get("mysqlconf", "db_name")user = cf.get("mysqlconf", "user")password = cf.get("mysqlconf", "password")# ======== MySql base operating ===================class DB: def __init__(self): try: # Connect to the database self.connection = pymysql.connect(host=host, port=int(port), user=user, password=password, db=db, charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor) except pymysql.err.OperationalError as e: print("Mysql Error %d: %s" % (e.args[0], e.args[1])) # clear table data def clear(self, table_name): # real_sql = "truncate table " + table_name + ";" real_sql = "delete from " + table_name + ";" with self.connection.cursor() as cursor: cursor.execute("SET FOREIGN_KEY_CHECKS=0;") cursor.execute(real_sql) self.connection.commit() # insert sql statement def insert(self, table_name, table_data): for key in table_data: table_data[key] = "'"+str(table_data[key])+"'" key = ','.join(table_data.keys()) value = ','.join(table_data.values()) real_sql = "INSERT INTO " + table_name + " (" + key + ") VALUES (" + value + ")" #print(real_sql) with self.connection.cursor() as cursor: cursor.execute(real_sql) self.connection.commit() # close database def close(self): self.connection.close() # init data def init_data(self, datas): for table, data in datas.items(): self.clear(table) for d in data: self.insert(table, d) self.close()if __name__ == '__main__': db = DB() table_name = "sign_event" data = {'id':1,'name':'紅米','`limit`':2000,'status':1,'address':'北京會展中心','start_time':'2016-08-20 00:25:42'} table_name2 = "sign_guest" data2 = {'realname':'alen','phone':12312341234,'email':'alen@mail.com','sign':0,'event_id':1} db.clear(table_name) db.insert(table_name, data) db.close()
首先,讀取 db_config.ini 設定檔。 建立 DB 類,__init__()方法初始化,通過 pymysql.connect()串連資料庫。
因為這裡只用到資料庫表的清除和插入,所以只建立 clear()和 insert()兩個方法。其中,insert()方法對數 據的插入做了簡單的格式轉化,可將字典轉化成 SQL 插入語句,這樣格式轉化了方便了資料庫表資料的建立。
最後,通過 close()方法用於關閉資料庫連接。
4.2.3、接下來接下來 建立測試資料 ,.../db_fixture/ test_data.py
import syssys.path.append('../db_fixture')try: from mysql_db import DBexcept ImportError: from .mysql_db import DB# create datadatas = { 'sign_event':[ {'id':1,'name':'紅米Pro發布會','`limit`':2000,'status':1,'address':'北京會展中心','start_time':'2017-08-20 14:00:00'}, {'id':2,'name':'可參加人數為0','`limit`':0,'status':1,'address':'北京會展中心','start_time':'2017-08-20 14:00:00'}, {'id':3,'name':'目前狀態為0關閉','`limit`':2000,'status':0,'address':'北京會展中心','start_time':'2017-08-20 14:00:00'}, {'id':4,'name':'發布會已結束','`limit`':2000,'status':1,'address':'北京會展中心','start_time':'2001-08-20 14:00:00'}, {'id':5,'name':'小米5發布會','`limit`':2000,'status':1,'address':'北京國家會議中心','start_time':'2017-08-20 14:00:00'}, ], 'sign_guest':[ {'id':1,'realname':'alen','phone':13511001100,'email':'alen@mail.com','sign':0,'event_id':1}, {'id':2,'realname':'has sign','phone':13511001101,'email':'sign@mail.com','sign':1,'event_id':1}, {'id':3,'realname':'tom','phone':13511001102,'email':'tom@mail.com','sign':0,'event_id':5}, ],}# Inster table datasdef init_data(): DB().init_data(datas)if __name__ == '__main__': init_data()
init_data()函數用於讀取 datas 字典中的資料,調用 DB 類中的 clear()方法清除資料庫,然後,調用 insert() 方法插入表資料。
4.2.4、編寫 介面測試案例 。建立添加發布會介面測試檔案.../interface/ add_event_test.py
import unittestimport requestsimport os, sysparentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))sys.path.insert(0, parentdir)from db_fixture import test_dataclass AddEventTest(unittest.TestCase): ''' 添加發布會 ''' def setUp(self): self.base_url = "http://127.0.0.1:8000/api/add_event/" def tearDown(self): print(self.result) def test_add_event_all_null(self): ''' 所有參數為空白 ''' payload = {'eid':'','':'','limit':'','address':"",'start_time':''} r = requests.post(self.base_url, data=payload) self.result = r.json() self.assertEqual(self.result['status'], 10021) self.assertEqual(self.result['message'], 'parameter error') def test_add_event_eid_exist(self): ''' id已經存在 ''' payload = {'eid':1,'name':'一加4發布會','limit':2000,'address':"深圳寶體",'start_time':'2017'} r = requests.post(self.base_url, data=payload) self.result = r.json() self.assertEqual(self.result['status'], 10022) self.assertEqual(self.result['message'], 'event id already exists') def test_add_event_name_exist(self): ''' 名稱已經存在 ''' payload = {'eid':11,'name':'紅米Pro發布會','limit':2000,'address':"深圳寶體",'start_time':'2017'} r = requests.post(self.base_url,data=payload) self.result = r.json() self.assertEqual(self.result['status'], 10023) self.assertEqual(self.result['message'], 'event name already exists') def test_add_event_data_type_error(self): ''' 日期格式錯誤 ''' payload = {'eid':11,'name':'一加4手機發布會','limit':2000,'address':"深圳寶體",'start_time':'2017'} r = requests.post(self.base_url,data=payload) self.result = r.json() self.assertEqual(self.result['status'], 10024) self.assertIn('start_time format error.', self.result['message']) def test_add_event_success(self): ''' 添加成功 ''' payload = {'eid':11,'name':'一加4手機發布會','limit':2000,'address':"深圳寶體",'start_time':'2017-05-10 12:00:00'} r = requests.post(self.base_url,data=payload) self.result = r.json() self.assertEqual(self.result['status'], 200) self.assertEqual(self.result['message'], 'add event success')if __name__ == '__main__': test_data.init_data() # 初始化介面測試資料 unittest.main()
在測試介面之前,調用test_data.py檔案中的init_data()方法初始化資料庫中的測試資料。
建立AddEventTest測試類別繼承 unittest.TestCase 類,通過建立測試案例,調用相關介面,並驗證介面返回 的資料。
4.2.5、建立 run_tests.py 檔案
當開發的介面達到一定數量後,就需要考慮 分檔案分目錄 的來 劃分 介面測試案例,如何批量的執行不同檔案目錄下的用例呢?unittest單元測試架構提供的 discover() 方法可以協助我們做到這一點。並使用 HTMLTestRunner 擴充產生 HTML 格式的測試報告。
import time, syssys.path.append('./interface')sys.path.append('./db_fixture')from HTMLTestRunner import HTMLTestRunnerimport unittestfrom db_fixture import test_data# 指定測試案例為當前檔案夾下的 interface 目錄test_dir = './interface'discover = unittest.defaultTestLoader.discover(test_dir, pattern='*_test.py')if __name__ == "__main__": test_data.init_data() # 初始化介面測試資料 now = time.strftime("%Y-%m-%d %H_%M_%S") filename = './report/' + now + '_result.html' fp = open(filename, 'wb') runner = HTMLTestRunner(stream=fp, title='Guest Manage System Interface Test Report', description='Implementation Example with: ') runner.run(discover) 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/目錄下面。如,一張完整的介面自動化測試報告。
以上就是本文的全部內容,希望對大家的學習有所協助,也希望大家多多支援幫客之家。