1. 程式人生 > 實用技巧 >Python+request+unittest實現介面測試框架整合例項

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, 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':'[email protected]','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 sys
sys.path.append('../db_fixture')
try:
  from mysql_db import DB
except ImportError:
  from .mysql_db import DB
 
# create data
datas = {
  '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':'[email protected]','sign':0,'event_id':1},
    {'id':2,'realname':'has sign','phone':13511001101,'email':'[email protected]','sign':1,'event_id':1},
    {'id':3,'realname':'tom','phone':13511001102,'email':'[email protected]','sign':0,'event_id':5},
  ],
}
 
# Inster table datas
def 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` `unittest`

`import` `requests`

`import` `os, sys`

`parentdir ``=` `os.path.dirname(os.path.dirname(os.path.abspath(__file__)))`

`sys.path.insert(``0``, parentdir)`

`from` `db_fixture ``import` `test_data`

`class` `AddEventTest(unittest.TestCase):`

`''' 添加發佈會 '''`

`def` `setUp(``self``):`

`self``.base_url ``=` `"[http://127.0.0.1:8000/api/add_event/](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, sys
sys.path.append('./interface')
sys.path.append('./db_fixture')
from HTMLTestRunner import HTMLTestRunner
import unittest
from 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/目錄下面。如下圖,一張完整的介面自動化測試報告。



作者:蜀山客e
連結:https://www.jianshu.com/p/e2ab94009f98
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。