1. 程式人生 > 實用技巧 >【python】python_介面自動化測試框架

【python】python_介面自動化測試框架

python_介面自動化測試框架

本文總結分享介紹介面測試框架開發,環境使用python3+selenium3+unittest+ddt+requests測試框架及ddt資料驅動,採用Excel管理測試用例等整合測試資料功能,以及使用HTMLTestRunner來生成測試報告,目前有開源的poman、Jmeter等介面測試工具,為什麼還要開發介面測試框架呢?因介面測試工具也有存在幾點不足。

  • 測試資料不可控制。比如介面返回資料不可控,就無法自動斷言介面返回的資料,不能斷定是介面程式引起,還是測試資料變化引起的錯誤,所以需要做一些初始化測試資料。介面工具沒有具備初始化測試資料功能,無法做到真正的介面測試自動化。
  • 無法測試加密介面。實際專案中,多數介面不是可以隨便呼叫,一般情況無法摸擬和生成加密演算法。如時間戳和MDB加密演算法,一般介面工具無法摸擬。
  • 擴充套件能力不足。開源的介面測試工具無法實現擴充套件功能。比如,我們想生成不同格式的測試報告,想將測試報告發送到指定郵箱,又想讓介面測試整合到CI中,做持續整合定時任務。

測試框架處理流程

測試框架處理過程如下:

  1. 首先初始化清空資料庫表的資料,向資料庫插入測試資料;
  2. 呼叫被測試系統提供的介面,先資料驅動讀取excel用例一行資料;
  3. 傳送請求資料,根據傳引數據,向資料庫查詢得到對應的資料;
  4. 將查詢的結果組裝成JSON格式的資料,同時根據返回的資料值與Excel的值對比判斷,並寫入結果至指定Excel測試用例表格;
  5. 通過單元測試框架斷言介面返回的資料,並生成測試報告,最後把生成最新的測試報告HTML檔案傳送指定的郵箱。

測試框架結構目錄介紹

目錄結構介紹如下:

  • config/: 檔案路徑配置
  • database/: 測試用例模板檔案及資料庫和傳送郵箱配置檔案
  • db_fixture/: 初始化介面測試資料
  • lib/: 程式核心模組。包含有excel解析讀寫、傳送郵箱、傳送請求、生成最新測試報告檔案
  • package/: 存放第三方庫包。如HTMLTestRunner,用於生成HTML格式測試報告
  • report/: 生成介面自動化測試報告
  • testcase/: 用於編寫介面自動化測試用例
  • run_demo.py: 執行所有介面測試用例的主程式
  • GitHub專案地址: https://github.com/yingoja/DemoAPI

資料庫封裝

 1 [tester]
 2 name = Jason
 3 
 4 [mysqlconf]
 5 host = 127.0.0.1
 6 port = 3306
 7 user = root
 8 password = 123456
 9 db_name = guest
10 
11 [user]
12 # 傳送郵箱伺服器
13 HOST_SERVER = smtp.163.com
14 # 郵件發件人
15 FROM = 111@163.com
16 # 郵件收件人
17 TO = 222@126.com
18 # 傳送郵箱使用者名稱/密碼
19 user = aaa
20 password = aaa
21 # 郵件主題
22 SUBJECT = 釋出會系統介面自動化測試報告
 1 #!/usr/bin/env python
 2 # _*_ coding:utf-8 _*_
 3 __author__ = 'YinJia'
 4 
 5 import os,sys
 6 sys.path.append(os.path.dirname(os.path.dirname(__file__)))
 7 from config import setting
 8 from pymysql import connect,cursors
 9 from pymysql.err import OperationalError
10 import configparser as cparser
11 
12 # --------- 讀取config.ini配置檔案 ---------------
13 cf = cparser.ConfigParser()
14 cf.read(setting.TEST_CONFIG,encoding='UTF-8')
15 host = cf.get("mysqlconf","host")
16 port = cf.get("mysqlconf","port")
17 user = cf.get("mysqlconf","user")
18 password = cf.get("mysqlconf","password")
19 db = cf.get("mysqlconf","db_name")
20 
21 class DB:
22     """
23     MySQL基本操作
24     """
25     def __init__(self):
26         try:
27             # 連線資料庫
28             self.conn = connect(host = host,
29                                 user = user,
30                                 password = password,
31                                 db = db,
32                                 charset = 'utf8mb4',
33                                 cursorclass = cursors.DictCursor
34                                 )
35         except OperationalError as e:
36             print("Mysql Error %d: %s" % (e.args[0],e.args[1]))
37 
38    # 清除表資料
39     def clear(self,table_name):
40         real_sql = "delete from " + table_name + ";"
41         with self.conn.cursor() as cursor:
42              # 取消表的外來鍵約束
43             cursor.execute("SET FOREIGN_KEY_CHECKS=0;")
44             cursor.execute(real_sql)
45         self.conn.commit()
46 
47     # 插入表資料
48     def insert(self, table_name, table_data):
49         for key in table_data:
50             table_data[key] = "'"+str(table_data[key])+"'"
51         key   = ','.join(table_data.keys())
52         value = ','.join(table_data.values())
53         real_sql = "INSERT INTO " + table_name + " (" + key + ") VALUES (" + value + ")"
54 
55         with self.conn.cursor() as cursor:
56             cursor.execute(real_sql)
57         self.conn.commit()
58 
59     # 關閉資料庫
60     def close(self):
61         self.conn.close()
62 
63     # 初始化資料
64     def init_data(self, datas):
65         for table, data in datas.items():
66             self.clear(table)
67             for d in data:
68                 self.insert(table, d)
69         self.close()
 1 #!/usr/bin/env python
 2 # _*_ coding:utf-8 _*_
 3 __author__ = 'YinJia'
 4 
 5 import sys, time, os
 6 sys.path.append(os.path.dirname(os.path.dirname(__file__)))
 7 from db_fixture.mysql_db import DB
 8 
 9 # 定義過去時間
10 past_time = time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(time.time()-100000))
11 # 定義將來時間
12 future_time = time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(time.time()+10000))
13 
14 # 建立測試資料
15 datas = {
16     # 釋出會表資料
17     'sign_event':[
18         {'id':1,'name':'紅米Pro釋出會','`limit`':2000,'status':1,'address':'北京會展中心','start_time':future_time},
19         {'id':2,'name':'蘋果iphon6釋出會','`limit`':1000,'status':1,'address':'寶安體育館','start_time':future_time},
20         {'id':3,'name':'華為榮耀8釋出會','`limit`':2000,'status':0,'address':'深圳福田會展中心','start_time':future_time},
21         {'id':4,'name':'蘋果iphon8釋出會','`limit`':2000,'status':1,'address':'深圳灣體育中心','start_time':past_time},
22         {'id':5,'name':'小米5釋出會','`limit`':2000,'status':1,'address':'北京國家會議中心','start_time':future_time},
23     ],
24     # 嘉賓表資料
25     'sign_guest':[
26         {'id':1,'realname':'Tom','phone':13511886601,'email':'[email protected]','sign':0,'event_id':1},
27         {'id':2,'realname':'Jason','phone':13511886602,'email':'[email protected]','sign':1,'event_id':1},
28         {'id':3,'realname':'Jams','phone':13511886603,'email':'[email protected]','sign':0,'event_id':5},
29     ],
30 }
31 
32 # 測試資料插入表
33 def init_data():
34     DB().init_data(datas)
 1 #!/usr/bin/env python
 2 # _*_ coding:utf-8 _*_
 3 __author__ = 'YinJia'
 4 
 5 import os,sys
 6 BASE_DIR = os.path.dirname(os.path.dirname(__file__))
 7 sys.path.append(BASE_DIR)
 8 
 9 # 配置檔案
10 TEST_CONFIG =  os.path.join(BASE_DIR,"database","config.ini")
11 # 測試用例模板檔案
12 SOURCE_FILE = os.path.join(BASE_DIR,"database","DemoAPITestCase.xlsx")
13 # excel測試用例結果檔案
14 TARGET_FILE = os.path.join(BASE_DIR,"report","excelReport","DemoAPITestCase.xlsx")
15 # 測試用例報告
16 TEST_REPORT = os.path.join(BASE_DIR,"report")
17 # 測試用例程式檔案
18 TEST_CASE = os.path.join(BASE_DIR,"testcase")

程式核心模組

 1 #!/usr/bin/env python
 2 # _*_ coding:utf-8 _*_
 3 __author__ = 'YinJia'
 4 
 5 import os
 6 
 7 def new_report(testreport):
 8     """
 9     生成最新的測試報告檔案
10     :param testreport:
11     :return:返回檔案
12     """
13     lists = os.listdir(testreport)
14     lists.sort(key=lambda fn: os.path.getmtime(testreport + "\\" + fn))
15     file_new = os.path.join(testreport,lists[-1])
16     return file_new
 1 #!/usr/bin/env python
 2 # _*_ coding:utf-8 _*_
 3 __author__ = 'YinJia'
 4 
 5 import xlrd
 6 
 7 class ReadExcel():
 8     """讀取excel檔案資料"""
 9     def __init__(self,fileName, SheetName="Sheet1"):
10         self.data = xlrd.open_workbook(fileName)
11         self.table = self.data.sheet_by_name(SheetName)
12 
13         # 獲取總行數、總列數
14         self.nrows = self.table.nrows
15         self.ncols = self.table.ncols
16     def read_data(self):
17         if self.nrows > 1:
18             # 獲取第一行的內容,列表格式
19             keys = self.table.row_values(0)
20             listApiData = []
21             # 獲取每一行的內容,列表格式
22             for col in range(1, self.nrows):
23                 values = self.table.row_values(col)
24                 # keys,values組合轉換為字典
25                 api_dict = dict(zip(keys, values))
26                 listApiData.append(api_dict)
27             return listApiData
28         else:
29             print("表格是空資料!")
30             return None
 1 #!/usr/bin/env python
 2 # _*_ coding:utf-8 _*_
 3 __author__ = 'YinJia'
 4 
 5 import os,sys,json
 6 sys.path.append(os.path.dirname(os.path.dirname(__file__)))
 7 
 8 
 9 class SendRequests():
10     """傳送請求資料"""
11     def sendRequests(self,s,apiData):
12         try:
13             #從讀取的表格中獲取響應的引數作為傳遞
14             method = apiData["method"]
15             url = apiData["url"]
16             if apiData["params"] == "":
17                 par = None
18             else:
19                 par = eval(apiData["params"])
20             if apiData["headers"] == "":
21                 h = None
22             else:
23                 h = eval(apiData["headers"])
24             if apiData["body"] == "":
25                 body_data = None
26             else:
27                 body_data = eval(apiData["body"])
28             type = apiData["type"]
29             v = False
30             if type == "data":
31                 body = body_data
32             elif type == "json":
33                 body = json.dumps(body_data)
34             else:
35                 body = body_data
36 
37             #傳送請求
38             re = s.request(method=method,url=url,headers=h,params=par,data=body,verify=v)
39             return re
40         except Exception as e:
41             print(e)
 1 #!/usr/bin/env python
 2 # _*_ coding:utf-8 _*_
 3 __author__ = 'YinJia'
 4 
 5 import os,sys
 6 sys.path.append(os.path.dirname(os.path.dirname(__file__)))
 7 from config import setting
 8 import smtplib
 9 from lib.newReport import new_report
10 import configparser
11 from email.mime.text import MIMEText
12 from email.mime.multipart import MIMEMultipart
13 
14 
15 def send_mail(file_new):
16     """
17     定義傳送郵件
18     :param file_new:
19     :return: 成功:打印發送郵箱成功;失敗:返回失敗資訊
20     """
21     f = open(file_new,'rb')
22     mail_body = f.read()
23     f.close()
24     #傳送附件
25     con = configparser.ConfigParser()
26     con.read(setting.TEST_CONFIG,encoding='utf-8')
27     report = new_report(setting.TEST_REPORT)
28     sendfile = open(report,'rb').read()
29     # --------- 讀取config.ini配置檔案 ---------------
30     HOST = con.get("user","HOST_SERVER")
31     SENDER = con.get("user","FROM")
32     RECEIVER = con.get("user","TO")
33     USER = con.get("user","user")
34     PWD = con.get("user","password")
35     SUBJECT = con.get("user","SUBJECT")
36 
37     att = MIMEText(sendfile,'base64','utf-8')
38     att["Content-Type"] = 'application/octet-stream'
39     att.add_header("Content-Disposition", "attachment", filename=("gbk", "", report))
40 
41     msg = MIMEMultipart('related')
42     msg.attach(att)
43     msgtext = MIMEText(mail_body,'html','utf-8')
44     msg.attach(msgtext)
45     msg['Subject'] = SUBJECT
46     msg['from'] = SENDER
47     msg['to'] = RECEIVER
48 
49     try:
50         server = smtplib.SMTP()
51         server.connect(HOST)
52         server.starttls()
53         server.login(USER,PWD)
54         server.sendmail(SENDER,RECEIVER,msg.as_string())
55         server.quit()
56         print("郵件傳送成功!")
57     except Exception as  e:
58         print("失敗: " + str(e))
 1 #!/usr/bin/env python
 2 # _*_ coding:utf-8 _*_
 3 __author__ = 'YinJia'
 4 
 5 import os,sys
 6 sys.path.append(os.path.dirname(os.path.dirname(__file__)))
 7 import shutil
 8 from config import setting
 9 from openpyxl import load_workbook
10 from openpyxl.styles import Font,Alignment
11 from openpyxl.styles.colors import RED,GREEN,DARKYELLOW
12 import configparser as cparser
13 
14 # --------- 讀取config.ini配置檔案 ---------------
15 cf = cparser.ConfigParser()
16 cf.read(setting.TEST_CONFIG,encoding='UTF-8')
17 name = cf.get("tester","name")
18 
19 class WriteExcel():
20     """檔案寫入資料"""
21     def __init__(self,fileName):
22         self.filename = fileName
23         if not os.path.exists(self.filename):
24             # 檔案不存在,則拷貝模板檔案至指定報告目錄下
25             shutil.copyfile(setting.SOURCE_FILE,setting.TARGET_FILE)
26         self.wb = load_workbook(self.filename)
27         self.ws = self.wb.active
28 
29     def write_data(self,row_n,value):
30         """
31         寫入測試結果
32         :param row_n:資料所在行數
33         :param value: 測試結果值
34         :return: 無
35         """
36         font_GREEN = Font(name='宋體', color=GREEN, bold=True)
37         font_RED = Font(name='宋體', color=RED, bold=True)
38         font1 = Font(name='宋體', color=DARKYELLOW, bold=True)
39         align = Alignment(horizontal='center', vertical='center')
40         # 獲數所在行數
41         L_n = "L" + str(row_n)
42         M_n = "M" + str(row_n)
43         if value == "PASS":
44             self.ws.cell(row_n, 12, value)
45             self.ws[L_n].font = font_GREEN
46         if value == "FAIL":
47             self.ws.cell(row_n, 12, value)
48             self.ws[L_n].font = font_RED
49         self.ws.cell(row_n, 13, name)
50         self.ws[L_n].alignment = align
51         self.ws[M_n].font = font1
52         self.ws[M_n].alignment = align
53         self.wb.save(self.filename)

介面測試用例編寫

 1 #!/usr/bin/env python
 2 # _*_ coding:utf-8 _*_
 3 __author__ = 'YinJia'
 4 
 5 import os,sys
 6 sys.path.append(os.path.dirname(os.path.dirname(__file__)))
 7 import unittest,requests,ddt
 8 from config import setting
 9 from lib.readexcel import ReadExcel
10 from lib.sendrequests import SendRequests
11 from lib.writeexcel import WriteExcel
12 
13 testData = ReadExcel(setting.SOURCE_FILE, "Sheet1").read_data()
14 
15 @ddt.ddt
16 class Demo_API(unittest.TestCase):
17     """釋出會系統"""
18     def setUp(self):
19         self.s = requests.session()
20 
21     def tearDown(self):
22         pass
23 
24     @ddt.data(*testData)
25     def test_api(self,data):
26         # 獲取ID欄位數值,擷取結尾數字並去掉開頭0
27         rowNum = int(data['ID'].split("_")[2])
28         # 傳送請求
29         re = SendRequests().sendRequests(self.s,data)
30         # 獲取服務端返回的值
31         self.result = re.json()
32         # 獲取excel表格資料的狀態碼和訊息
33         readData_code = int(data["status_code"])
34         readData_msg = data["msg"]
35         if readData_code == self.result['status'] and readData_msg == self.result['message']:
36             OK_data = "PASS"
37             WriteExcel(setting.TARGET_FILE).write_data(rowNum + 1,OK_data)
38         if readData_code != self.result['status'] or readData_msg != self.result['message']:
39             NOT_data = "FAIL"
40             WriteExcel(setting.TARGET_FILE).write_data(rowNum + 1,NOT_data)
41         self.assertEqual(self.result['status'], readData_code, "返回實際結果是->:%s" % self.result['status'])
42         self.assertEqual(self.result['message'], readData_msg, "返回實際結果是->:%s" % self.result['message'])
43 
44 if __name__=='__main__':
45     unittest.main()

整合測試報告

 1 #!/usr/bin/env python
 2 # _*_ coding:utf-8 _*_
 3 __author__ = 'YinJia'
 4 
 5 
 6 import os,sys
 7 sys.path.append(os.path.dirname(__file__))
 8 from config import setting
 9 import unittest,time
10 from HTMLTestRunner import HTMLTestRunner
11 from lib.sendmail import send_mail
12 from lib.newReport import new_report
13 from db_fixture import test_data
14 from package.HTMLTestRunner import HTMLTestRunner
15 
16 def add_case(test_path=setting.TEST_CASE):
17     """載入所有的測試用例"""
18     discover = unittest.defaultTestLoader.discover(test_path, pattern='*API.py')
19     return discover
20 
21 def run_case(all_case,result_path=setting.TEST_REPORT):
22     """執行所有的測試用例"""
23 
24     # 初始化介面測試資料
25     test_data.init_data()
26 
27     now = time.strftime("%Y-%m-%d %H_%M_%S")
28     filename =  result_path + '/' + now + 'result.html'
29     fp = open(filename,'wb')
30     runner = HTMLTestRunner(stream=fp,title='釋出會系統介面自動化測試報告',
31                             description='環境:windows 7 瀏覽器:chrome',
32                             tester='Jason')
33     runner.run(all_case)
34     fp.close()
35     report = new_report(setting.TEST_REPORT) #呼叫模組生成最新的報告
36     send_mail(report) #呼叫傳送郵件模組
37 
38 if __name__ =="__main__":
39     cases = add_case()
40     run_case(cases)

測試結果展示

  • HTML測試結果報告:

  • Excel測試用例結果

  • 郵件收到的測試報告