? ? ? ? ? ? 本問主要介紹用excel表格來管理接口用例,采用python+unittest測試框架,結合ddt數(shù)據(jù)驅動,最后結合BeautifulReport報告插件,生成最終的測試報告
首先,來3張圖,了解輸入數(shù)據(jù),輸出結果
?????1、需要測試的接口case:execel表格管理

? ??2、請求的body:request_data.py文件中字典req_data,用來存放所有case請求的body

? ? 3、利用unittest+ddt+BeautifulReport生成HTML測試報告:

其次,附上整個項目的結構圖

最后,分解項目運行的細節(jié)內容
1、項目的主運行文件:run_ddt.py
(1)、導入我們run_ddt.py文件運行所需要的第三方包
# coding=utf-8
import unittest
import time
import os
from BeautifulReport import BeautifulReport
(2)、生成我們需要的report路徑
curpath = os.path.dirname(os.path.realpath(__file__))
reprot_path = os.path.join(curpath, "report")
(3)、匹配該項目里,以test開頭的文件,并添加成一個unittest測試集
def add_case(casepath=curpath, rule="test_*.py"):
? ? discover = unittest.defaultTestLoader.discover(casepath, pattern=rule)
? ? return discover
(4)、得到了測試集,便可以運行整個測試集里面的測試用例
def run_case(all_case, reportpath=reprot_path):
? ? now = time.strftime("%Y%m%d%H%M%S")
? ? print('測試報告生成地址: %s' % reportpath)
? ? BeautifulReport(all_case).report(description='用例執(zhí)行情況', filename='測試報告_' + str(now), report_dir=reportpath)
# 該文件的main函數(shù)入口:
if __name__ == '__main__':
? ? cases = add_case()
? ? run_case(cases)
? ? ? ? 寫到上面第3步的時候,你就會聯(lián)想到,我們后續(xù)肯定會編寫一個test_開頭的py文件,而里面就是我們的測試內容。
? ? ? ? 是的,我們第2個文件,就是編寫我們的測試代碼,也可以說是我們的測試思路或者是測試的步驟。
2、測試思路:test_case_all.py
(1)、導入該文件所運行的包,以及從公用模塊導入公用函數(shù)
from myrequests import MyRequests
from common.operationExcel import OperationExcel
from common.operationReqData import OperationReqDate
from common.dependentData import DependentData
from ddt import ddt, data, unpack
from common.operationSQL import connectSQL
from config import *
import json
import unittest
(2)、我們是從一個excel的sheet表拿的數(shù)據(jù),要想把這些數(shù)據(jù)利用ddt來驅動,就需要把整個excel表的數(shù)據(jù)全部拿出來,然后再利用ddt來分割數(shù)據(jù),在用切割的數(shù)據(jù)進行單個case測試
class GetReqData(object):
# 初始化excel操作模塊類,才能調用該類下的函數(shù)方法
? ? def __init__(self):
? ? ? ? self.operation_excel = OperationExcel()
#? 利用excel類里面的方法,獲取excel表格的所有數(shù)據(jù)
? ? def get_data(self):
? ? ? ? exe_data_all = []? ? # 定義一個空的列表,存放excel表格的數(shù)據(jù)
? ? ? ? rows_count = self.operation_excel.get_all_lines()? ? # 獲取表格中有多少行數(shù)據(jù)
? ? ? ? for i in range(1, rows_count):? ? # 循環(huán)遍歷取excel表格的數(shù)據(jù),去除第一行
? ? ? ? ? ? exe_data = self.operation_excel.get_a_row_data(i)? ? # 把每一行的數(shù)據(jù)都取出
? ? ? ? ? ? exe_data_all.append(tuple(exe_data))? ? # 取出的數(shù)據(jù),都添加到定義空列表中
? ? ? ? return exe_data_all? ? # 取值完成后,把所有的數(shù)據(jù)返回出去
# 單獨的把獲取數(shù)據(jù)函數(shù)進行調用一次,這樣ddt數(shù)據(jù)驅動,才有數(shù)據(jù)作為參數(shù)傳入
get_req_data = GetReqData()
req_data = get_req_data.get_data()
(3)、ddt來驅動excel表的數(shù)據(jù),獲取到的excel數(shù)據(jù)是一個list類型,提取每一行的數(shù)據(jù)就顯示輕松多了。提取完數(shù)據(jù),就可以進行request請求測試了。
# 采用ddt數(shù)據(jù)驅動,在運行的類前,就需要先運行ddt的裝飾器函數(shù),故需要在Run類前加上@ddt
@ddt
class Run(unittest.TestCase):
? ? #? 集成unittest.TestCase方法,然而需要初始化,在unittest里__init__函數(shù)無法使用,所以我們就用到unittest里的setUp、?tearDown這樣函數(shù)來做類函數(shù)的初始化,這里初始化只需要運行一次,這里我采用了setUpClass這個函數(shù)來實現(xiàn)
? ? @classmethod
? ? def setUpClass(cls):
? ? ? ? print('------執(zhí)行開始------')
? ? ? ? cls.operation_excel = OperationExcel()
? ? ? ? cls.operation_req_data = OperationReqDate()
? ? ? ? cls.dependent_data = DependentData()
? ? ? ? cls.m = MyRequests()
? ? ? ? cls.host = HOST? ? # 從config文件獲取host,這樣切換地址不用改excel表的url內容
? ? ? ? cls.new_data_dict = {}
? ? @classmethod
? ? def tearDownClass(cls):
? ? ? ? print('------執(zhí)行完畢------')
#? ? ?初始化工作已完成,那就進入我們重點、重點、重點了
? ? @data(*req_data)? ? # ddt下data可以把數(shù)據(jù)進行切分返回數(shù)據(jù),具體可參照ddt使用
? ? @unpack? ? # ddt下的一個方法,目的是把每一行數(shù)據(jù)分開傳參,具體使用ddt詳解
? ? def test_case(cls,? *exe_data):
? ? ? ??# 這里是判斷需要執(zhí)行SQL語句
? ? ? ? if exe_data[3] == "SQL":? ? ? ?
? ? ? ? ? ? sql = exe_data[5]
? ? ? ? ? ? connectSQL(exe_data[7], sql, cls.new_data_dict)
'''
這里判斷case是都需要執(zhí)行(運行的流程重點就在此)
我們從每一行數(shù)據(jù)取出來是一個list,根據(jù)list的下標,獲取excel表格的值;
? ? 1、取決于該case是否運行,如果運行,就往下取值,反之,則不用管;
? ? 2、獲取該case請求的body值,根據(jù)excel的req_data字段,取對應的值
? ? 3、如果該case有依賴,就需要走依賴函數(shù),進行鍵位值的替換,實現(xiàn)實時數(shù)據(jù)變動;
? ? 4、進行接口的請求(如果沒有依賴,則可以跳過第3步)
? ? 5、進行預期結果與實際結果的對比
? ? 6、最后,如果該case需要提取某個字段的值,根據(jù)鍵位,在返回的內容中進行提取
? ? 注意:new_data_dict這個字典,是存放替換的值,格式是key=value,key是我們自定義的名稱,value則是從返回值提取的值,提取數(shù)據(jù)必須在替換數(shù)據(jù)之前就有值,不然會報錯,因為提取的數(shù)據(jù)沒有值,替換的時候就無法找到值進行替換。提取值是用了jsonpath的方法提取,替換則是采用了自己定義的,以"."的方式代替層級關系。
'''
????elif exe_data[3] == 'YES':? ? ? ?# 第1步,判斷是否運行
? ? ? ? ? ? req_data = cls.operation_req_data.get_req_data(exe_data[5])? ? # 第2步取body
? ? ? ? ? ? print('執(zhí)行的用例ID: ', exe_data[0])
? ? ? ? ? ? data = json.loads(exe_data[9])? ? # 數(shù)據(jù)轉換,怕數(shù)據(jù)格式錯誤。
? ? ? ? ? ? if exe_data[7] != '':? ? # 判斷請求的body是否有依賴,此處判斷值為有依賴
? ? ? ? ? ? ? ? req_data = cls.dependent_data.replace_req_data(exe_data[7], req_data, cls.new_data_dict)? ? # 第3步,有依賴,從提取值獲取進行替換(注:提取值必須有值)
? ? ? ? ? ? ? ? res = cls.m.myrequests(cls.host + exe_data[2], req_data, exe_data[4], exe_data[6])? ????? # 第4步,進行數(shù)據(jù)請求
? ? ? ? ? ? ? ? for key, value in data.items():? ? ? ? # 第5步,預期結果與實際結果的對比
? ? ? ? ? ? ? ? ? ? res_value = cls.dependent_data.replace_data(key, res)
? ? ? ? ? ? ? ? ? ? cls.assertEqual(value, res_value)
? ? ? ? ? ? ? ? if exe_data[8] != '':? ? ? ? # 判斷是否需要提取
? ? ? ? ? ? ? ? ? ? cls.new_data_dict = cls.dependent_data.dependent_data(exe_data[8], res, cls.new_data_dict)? ? ? ? # 第6步,根據(jù)鍵位,在返回的內容提取值
? ? ? ? ? ? else:
? ? ? ? ? ? ? ? res = cls.m.myrequests(cls.host + exe_data[2], req_data, exe_data[4], exe_data[6])? ? ????# 第4步,進行無body替換的接口請求
? ? ? ? ? ? ? ? for key, value in data.items():? ? # 第5步,預期結果與實際結果對比
? ? ? ? ? ? ? ? ? ? res_value = cls.dependent_data.replace_data(key, res)
? ? ? ? ? ? ? ? ? ? cls.assertEqual(value, res_value)
? ? ? ? ? ? ? ? if exe_data[8] != '':? ? #? 判斷是否需要提取
? ? ? ? ? ? ? ? ? ? cls.new_data_dict = cls.dependent_data.dependent_data(exe_data[8], res, cls.new_data_dict)? ? # 第6步,根據(jù)提取的鍵位,在返回值中提取對應的值
# 該文件的程序入口
if __name__ == '__main__':
?????unittest.main()
3、整個項目的脊柱已經(jīng)弄好,現(xiàn)在就需要各個內容來支配整個項目
?----從test文件整理出,我們可以察覺到缺少的函數(shù)文件,我們一一列出:
---1。excel表格的數(shù)據(jù)獲取方法
---2。請求body的數(shù)據(jù)獲取方法
---3。提取值的方法
---4。替換body的方法
---5。接口請求的方法
從這5點中,我們就來一一編寫需要的方法:
3-1、excel的獲取數(shù)據(jù)方法:operationExcel.py
????在test文件里,我們發(fā)現(xiàn)了這兩句代碼,屬于excel的操作
rows_count = self.operation_excel.get_all_lines()
exe_data = self.operation_excel.get_a_row_data(i)
????那么我們就需要在common公用文件下新建一個operationExcel.py文件,來針對excel表格數(shù)據(jù)的操作
# coding=utf-8
import xlrd, os, time, xlwt
from xlutils.copy import copy
class OperationExcel(object):
? ? def __init__(self, file_name=None, sheet_id=None):
? ? ? ? if file_name:
? ? ? ? ? ? self.file_name = file_name
? ? ? ? ? ? self.sheet_id = sheet_id
? ? ? ? else:
? ? ? ? ? ? self.file_name = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data/ApiList1.xlsx')
? ? ? ? self.data = self.get_data()
? ? # 獲取數(shù)據(jù)
? ? def get_data(self):
? ? ? ? data = xlrd.open_workbook(self.file_name)
? ? ? ? tables = data.sheets()[self.sheet_id]
? ? ? ? return tables
? ? # 獲取sheet下的行數(shù)
? ? def get_all_lines(self):
? ? ? ? tables = self.data
? ? ? ? return tables.nrows
? ? # 獲取某一行的內容
? ? def get_a_row_data(self, row_num):
? ? ? ? tables = self.data
? ? ? ? row_data = tables.row_values(row_num)
? ? ? ? return row_data
3-2、body的獲取數(shù)據(jù)方法:operationReqData.py
? ? 在test文件里,我們會發(fā)現(xiàn)以下的代碼:
req_data = cls.operation_req_data.get_req_data(exe_data[5])
? ? 這樣的代碼,是我們從excel表取標識字段,到request_data文件里req_data取對應key的value,這樣body就能取出來了
from data import request_data
class OperationReqDate(object):
? ? def __init__(self):
? ? ? ? self.data = request_data.req_data? # 修改req_data的文件名
? ? # 根據(jù)關鍵key來獲取req_data文件的內容
? ? def get_req_data(self, key):
? ? ? ? if key == '':
? ? ? ? ? ? return None
? ? ? ? return self.data.get(key)
3-3、根據(jù)excel的數(shù)據(jù),提取返回值的內容:dependentData.py
? ? ????我們在現(xiàn)實的測試中,往往發(fā)現(xiàn),這個接口運行的時候,會調用上一個接口的數(shù)據(jù),而且還有一些數(shù)據(jù)值,都是重復調用,總不可能請求一個接口,去多次調用其他接口吧,這樣就導致了接口的請求量變大了,增加了服務器的負載能力。
? ? ? ? 解決方案:我們在請求前,我們新建一個空的字典,自定義key來獲取對應的value值,成成一個新的字典,請求body需要的時候,就直接從這里取值,這樣就減少了請求次數(shù)。
? ? ? ? 然而,在test文件中,我們會發(fā)現(xiàn)有這樣的代碼存在:
if exe_data[8] != '':
? ? ? ? ? ? ? ? ? ? cls.new_data_dict = cls.dependent_data.dependent_data(exe_data[8], res, cls.new_data_dict)
? ? 這樣是進行判斷是否有提取值,有則需要提取,反之則不需要,然后我們的提取方法:
? ? 提取方法的思路:
? ? ? ? 1、根據(jù)依賴的鍵位,去遍歷返回的res,鍵位提取的格式:id=(result.id)
? ? ? ? 2、找到了鍵位的值后,把鍵位的key作為字典的key存放,找的值當做value存放,組成一個新的字典
# 根據(jù)exe表格中的key=(value)來獲取一個新增的dict-data
? ? def dependent_data(self, dependent_data, res, data_dict):
? ? ? ? exe_data = dependent_data.split('\n')
? ? ? ? # print('11: ', exe_data)
? ? ? ? for data in exe_data:
? ? ? ? ? ? data_value = data.split('=')
? ? ? ? ? ? # print('data_value:', data_value)
? ? ? ? ? ? dependent_key = data_value[1]
? ? ? ? ? ? value = self.replace_data(dependent_key, res)
? ? ? ? ? ? data_dict[data_value[0]] = value
? ? ? ? return data_dict
在此時,就會發(fā)現(xiàn)一個新的語句:
value = self.replace_data(dependent_key, res)
然而我們就需要在該文件下再創(chuàng)建一個函數(shù)方法,這里提取的方法是采用jsonpath:
def replace_data(self, data_key, data_value):
? ? ? ? """
? ? ? ? :param data_key: 依賴的key值
? ? ? ? :param data_value: 遍歷的返回頁面數(shù)據(jù)
? ? ? ? :return:
? ? ? ? """
? ? ? ? try:
? ? ? ? ? ? json_exe = parse(data_key)
? ? ? ? ? ? madles = json_exe.find(data_value)
? ? ? ? except Exception as msg:
? ? ? ? ? ? print(msg)
? ? ? ? return [madle.value for madle in madles][0]
????????這樣我們的程序就不會報錯,該方法的用途我也不做多解釋,網(wǎng)上有類似的專業(yè)講解。那么我們繼續(xù)我們項目其他方法解析
3-4、替換請求的body里的鍵位值:dependentData.py
? ? 在3-3中,我已經(jīng)講解了提取值的方法,主要是為了便于替換的時候需要,在新的一個字典里,我們只有傳入key,就能把之前接口請求返回的value取到,進行替換,就可以直接請求了,我在excel表的替換值的格式:id={{user_id}},格式可以根據(jù)自己喜歡來寫,切割點就需要重新變化下即可。
# 根據(jù)exe的表格key={{value}}去替換值
? ? def replace_req_data(self, dependent_data, req_data, new_data_dict):
? ? ? ? exe_data = dependent_data.split('\n')
? ? ? ? for data in exe_data:
? ? ? ? ? ? data_value = data.split('=')
? ? ? ? ? ? value = new_data_dict.get(data_value[1][2:-2])
? ? ? ? ? ? req_data = self.check_json_data.check_json_data(req_data, data_value[0], value)
? ? ? ? return req_data
? ? 在上面的方法中,發(fā)現(xiàn)有一行新的代碼
?req_data = self.check_json_data.check_json_data(req_data, data_value[0], value)
? ? 這行代碼是進行替換的操作,遍歷操作替換的工作量大,因此我們重新編寫一個文件來實現(xiàn)此功能:
3-4-1、數(shù)據(jù)替換方法:checkJsonData.py
? ? 遍歷我們的請求的body,根據(jù)對應的鍵位,去實現(xiàn)value的一個更新,實現(xiàn)數(shù)據(jù)更新功能
# coding=utf-8
from httprunner import exceptions, logger
from httprunner.compat import OrderedDict, basestring, is_py2
class Check_Json_Data(object):
? ? # 替換json數(shù)據(jù)中對應的value
? ? def change_json(self, json_content, query, new, delimiter='.'):
? ? ? ? raise_flag = False
? ? ? ? response_body = u"response body: {}\n".format(json_content)
? ? ? ? try:
? ? ? ? ? ? keys = query.split(delimiter)
? ? ? ? ? ? if len(keys) == 1:
? ? ? ? ? ? ? ? if isinstance(json_content, (list, basestring)):
? ? ? ? ? ? ? ? ? ? json_content[int(keys[0])] = new
? ? ? ? ? ? ? ? elif isinstance(json_content, dict):
? ? ? ? ? ? ? ? ? ? json_content[keys[0]] = new
? ? ? ? ? ? if len(keys) > 1:
? ? ? ? ? ? ? ? for key in keys:
? ? ? ? ? ? ? ? ? ? if isinstance(json_content, (list, basestring)):
? ? ? ? ? ? ? ? ? ? ? ? return self.change_json(json_content[int(key)], ".".join(keys[1:]), new, delimiter='.')
? ? ? ? ? ? ? ? ? ? elif isinstance(json_content, dict):
? ? ? ? ? ? ? ? ? ? ? ? return self.change_json(json_content[key], ".".join(keys[1:]), new, delimiter='.')
? ? ? ? except (KeyError, ValueError, IndexError):
? ? ? ? ? ? raise_flag = True
? ? ? ? if raise_flag:
? ? ? ? ? ? err_msg = u"Failed to extract! => {}\n".format(query)
? ? ? ? ? ? err_msg += response_body
? ? ? ? ? ? logger.log_error(err_msg)
? ? ? ? ? ? raise exceptions.ExtractFailure(err_msg)
# 數(shù)據(jù)替換的方法
? ? def check_json_data(self, old_req_data, dependent_key, values):
? ? ? ? """
? ? ? ? 把舊的請求數(shù)據(jù),根據(jù)鍵位,替換掉舊數(shù)據(jù)
? ? ? ? :param old_req_data: json文件的舊數(shù)據(jù)
? ? ? ? :param dependent_key: excel表中的鍵位值
? ? ? ? :param values: 獲取依賴的接口返回的鍵位值,也就是新值
? ? ? ? :return: 返回一個替換后的請求數(shù)據(jù)
? ? ? ? """
? ? ? ? self.change_json(old_req_data, dependent_key, values)
? ? ? ? return old_req_data
3-5、請求的方法:myrequests.py
? ? 我們采用request模塊進行url的請求,這里需要更新自己的token,各個平臺不同,token的取值也不同,這個因系統(tǒng)而異。
? ? 首先,我們需要提取token
????# 獲取token
? ? def login(self):
? ? ? ? global token
? ? ? ? if "Authorization" in self.s.headers.keys():? ? ? ? # 判斷是否存在token,如果有就直接跳過
? ? ? ? ? ? # print('--------token is exits!!---------')
? ? ? ? ? ? return self.s
? ? ? ? else:
? ? ? ? ? ? excel_data = self.operation_excel.get_a_row_data(1)
? ? ? ? ? ? url = self.host + excel_data[2]
? ? ? ? ? ? req_data = login_data
? ? ? ? ? ? res = self.s.post(url, json=req_data)
? ? ? ? ? ? r = res.content.decode('utf-8')
? ? ? ? ? ? r = json.loads(r)
? ? ? ? ? ? token = r['result']['token']
? ? ? ? ? ? self.s.headers.update({"Authorization": token})
? ? ? ? ? ? print("----------token create successfully!--------")
? ? ? ? ? ? return self.s
????????其次,封裝自己的請求方式。網(wǎng)上有很多種封裝方式,小伙伴可以選擇自己喜歡的封裝方式,這里我貼上我自己的封裝方式,方法不完美,能實現(xiàn)就好。
????# 自定義請求函數(shù)
? ? def myrequests(self, url, req_data, req_type, data_type):
? ? ? ? """
? ? ? ? 自定義請求函數(shù)
? ? ? ? :param url: 請求的url
? ? ? ? :param req_data: 請求的data
? ? ? ? :param req_type: 請求方式
? ? ? ? :param data_type: 數(shù)據(jù)的傳遞格式
? ? ? ? :return: res頁面結果
? ? ? ? """
? ? ? ? if req_type == "POST":? # 判斷請求方式:POST
? ? ? ? ? ? if data_type == 'JSON':? # 判斷請求參數(shù)的數(shù)據(jù)類型
? ? ? ? ? ? ? ? # post_data = json.loads(req_data)
? ? ? ? ? ? ? ? # res = self.login().post(url, json=req_data)
? ? ? ? ? ? ? ? res = self.login().post(url, json=req_data)
? ? ? ? ? ? elif data_type == '':
? ? ? ? ? ? ? ? res = self.login().post(url, data=req_data)
? ? ? ? ? ? else:
? ? ? ? ? ? ? ? res = self.login().post(url, data=req_data)
? ? ? ? elif req_type == "GET":
? ? ? ? ? ? if data_type == 'JSON':
? ? ? ? ? ? ? ? get_params = json.loads(req_data)
? ? ? ? ? ? ? ? res = self.login().get(url, params=get_params)
? ? ? ? ? ? elif data_type == '':
? ? ? ? ? ? ? ? res = self.login().get(url, params=req_data)
? ? ? ? ? ? else:
? ? ? ? ? ? ? ? res = self.login().get(url, params=req_data)
? ? ? ? elif req_type == "DELETE":
? ? ? ? ? ? if data_type == 'JSON':
? ? ? ? ? ? ? ? get_params = json.loads(req_data)
? ? ? ? ? ? ? ? res = self.login().delete(url, params=get_params)
? ? ? ? ? ? elif data_type == '':
? ? ? ? ? ? ? ? res = self.login().delete(url, params=req_data)
? ? ? ? ? ? else:
? ? ? ? ? ? ? ? res = self.login().delete(url, params=req_data)
? ? ? ? print("request_req_url: ", res.url)
? ? ? ? print("request_req_data: ", req_data)
? ? ? ? res = res.content
? ? ? ? res = json.loads(res.decode('utf-8'))
? ? ? ? print('res: ', res)
? ? ? ? return res
在附上配置文件內容:config
# coding=utf-8
HOST = "http://172.16.62.66"
# HOST = "http://172.16.62.71"
SQL_IP = "172.16.62.66"
# SQL_IP = "172.16.62.71"
db_message = {
? ? ? ? "host": SQL_IP,
? ? ? ? "username": "root",
? ? ? ? "password": "123456",
? ? ? ? "port": 3306,
? ? ? ? "charset": "utf8"
}
login_url = HOST + '/xxx-x'x'x'x/login/login'
login_data = {
? ? ? ? "mobile": "18100000000",
? ? ? ? "smsCode": "888888"
}
????????總結:項目的方法封裝不是很好,這里介紹我使用的辦法,如果有更好的方法,方便留言,多多研究,讓自動化測試更加完美。郵件的發(fā)送方法,請求頭文件的更新,我這邊都沒做,后期實現(xiàn)了,再更新。。