Python+request+unittest實現介面測試框架整合例項
1、為什麼要寫程式碼實現介面自動化
大家知道很多介面測試工具可以實現對介面的測試,如postman、jmeter、fiddler等等,而且使用方便,那麼為什麼還要寫程式碼實現介面自動化呢?工具雖然方便,但也不足之處:
測試資料不可控制
介面測試本質是對資料的測試,呼叫介面,輸入一些資料,隨後,介面返回一些資料。驗證介面返回資料的正確性。在用工具執行測試用例之前不得不手動向資料庫中插入測試資料。這樣我們的介面測試是不是就沒有那麼“自動化了”。
無法測試加密介面
這是介面測試工具的一大硬傷,如我們前面開發的介面用工具測試完全沒有問題,但遇到需要對介面參 數進行加密/解密的介面,例如 md5、base64、AES 等常見加密方式。本書第十一章會對加密介面進行介紹。 又或者介面的引數需要使用時間戳,也是工具很難模擬的。
擴充套件能力不足
當我們在享受工具所帶來的便利的同時,往往也會受制於工具所帶來的侷限。例如,我想將測試結果生 成 HMTL 格式測試報告,我想將測試報告發送到指定郵箱。我想對介面測試做定時任務。我想對介面測試做持續整合。這些需求都是工具難以實現的。
2、介面自動化測試設計
介面測試呼叫過程可以用下圖概括,增加了測試資料庫
image一般的 介面工具 測試過程:
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庫的更詳細的介紹可以看我之前介面測試基礎的文章:
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` `requests`
`url ``=` `"[http://127.0.0.1:8000/api/get_event_list/](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'``] ``=``=` `200`
`assert` `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` `unittest`
`import` `requests`
`class` `GetEventListTest(unittest.TestCase):`
`def` `setUp(``self``):`
`self``.base_url ``=` `"[http://127.0.0.1:8000/api/get_event_list/](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.cursors
import os
import 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