接口自動化測試框架-AIM

最近在作公司項目的自動化接口測試,在現有幾個小框架的基礎上,反覆研究和實踐,搭建了新的測試框架。利用業餘時間,把框架總結了下來。html

AIM框架介紹

AIM,是Automatic Interface Monitoring的簡稱,即自動化接口監測。是一種基於python unittest的自動化接口測試框架。python

設計思想

框架根據python語言的特色,結合了面向對象和麪向函數編程。算法

以高效編程爲主要目的,避免爲了封裝而封裝。輕配置,重編碼。sql

接口測試的主要處理對象是參數。若是徹底進行數據與代碼的分離,就會形成變量,傳參的冗餘,下降編程效率。數據庫

因而從不作數據與代碼分離出發,對於須要複用的參數,提取到類以外,視須要進行數據與代碼的分離。編程

作到有的放矢。兼顧效率和複用性,迭代分離,更具實用性。json

1552446647735

目錄結構

1557469790003

case:測試用例windows

common:公共函數,全局變量瀏覽器

config:配置路徑等cookie

data:數據文件

result:測試結果

util:工具類

run.py:用例執行入口

run_mail.py:執行後自動發送郵件入口

case

BaseCase

全部Case的基類。

封裝了requests庫的post和get函數req,用於發送請求。

調用assertEqual等方法,封裝了用例的斷言。好比檢查接口返回flag,檢查接口狀態200,檢查值相等。

項目Case

測試系統的用例。按模塊分別創建文件編寫腳本。

Env.py:環境配置,包括url處理,登陸對象login實例(用戶名、密碼),數據庫對象dao實例(數據庫鏈接)。

Public.py:公共模塊。存放本系統公共的變量、函數、用例等。

common

Func.py:公共函數,好比獲取時間日期,獲取隨機數,處理參數。

Login.py:登陸模塊,屬於各系統通用,故放於此目錄下。包括密碼加密,驗證碼處理,強制登陸。

Var.py:全局變量。好比token。

config

RelativePath.py:配置目錄、文件的相對路徑。

data

echarts數據存儲csv文件,項目接口清單等。

result

log:日誌。logging實現。支持輸出到文件和打印控制檯。文件暫時使用較少,主要打印控制檯便於調試。

接口調用記錄:輸出每一個測試方法調用接口的記錄,包括參數、響應、耗時等。

自動化測試報告:HTMLTestRunner.py實現的html頁面報告。

util

AutoCode.py:自動生成結構化測試代碼。

CSV.py:csv相關函數封裝。好比輸出接口調用記錄。

Excel.py:讀取和存儲excel文件。

Format.py:格式化。好比把瀏覽器複製的參數格式化爲代碼中帶有縮進的json。

HTMLTestRunner.py:用於輸出自動化測試報告。

Log.py:封裝日誌方法。

Mysql.py:數據庫相關操做。

Parewise.py:結對測試。一種測試技術,後文詳述。

Request.py:核心工具,封裝接口發送請求。

Mail.py:發送郵件。

run.py

執行測試用例入口,能夠選擇執行一個或多個系統,也能夠執行一個系統中一個或多個模塊。

核心模塊

BaseCase.req

經過requests封裝的發送接口請求的方法。

定義在BaseCase類的內部。

參數 說明
p 將url、headers、body、method統一封裝到一個json裏面進行處理。
method='post' 默認爲post方法。接口以post居多。
jsondata='json' 默認json參數。post方法的json或data,純json使用json參數便可。對於receive_json這種dict,採用data參數。
loglevel=3 默認爲3。日誌級別,輸出請求、響應信息到控制檯或接口調用記錄.csv。
rtext=None 一些get請求會返回html或pdf,在控制檯或csv文件中影響顯示,能夠指定文本進行替換。

發送請求,並計算耗時:

start = time.clock()
        if method == 'post':  # 關閉SSL認證
            if jsondata == 'json':
                r = self.timeoutTry("requests.post(p['url'], headers=p['headers'], json=p['body'], verify=False)", p)
            elif jsondata == 'data':
                r = self.timeoutTry("requests.post(p['url'], headers=p['headers'], data=p['body'], verify=False)", p)
            else:
                print('jsondata錯誤')
        elif method == 'get':
            r = self.timeoutTry("requests.get(p['url'], headers=p['headers'], params=p['body'])", p)
        else:
            print('method錯誤')
        end = time.clock()
        elapsed = decimal.Decimal("%.2f" % float(end - start))

其中的self.timeoutTry是爲了處理響應超時,會在後續博文中介紹。

Parewise

結對測試。接口參數通常是多個,因而比較適合採用parewise進行用例設計。

parewaise的概念能夠百度一下。

大概意思就是,大多數的bug都是條件的兩兩組合形成的,parewise就是針對兩兩組合的狀況,設計測試用例。

算法爲,若是某一組用例的組合結果,在其餘組合中均出現,就刪除該組用例,從而精簡用例。

windows下有微軟的PICT,txt文件錄入參數後,命名行執行,就出來結果了。

好比參數

1552448368346

執行後結果,只有31條,精簡了不少。

1552448425685

這個基本上一秒就出來結果了。

我本身參考網上算法寫的,就要慢的多。

估計後面有時間了再看看能不能調優。

parewise算法:

cp = []  # 笛卡爾積
s = []  # 兩兩拆分
for x in eval('itertools.product' + str(tuple(param_list))):
    cp.append(x)
    s.append([i for i in itertools.combinations(x, 2)])

del_row = []
s2 = copy.deepcopy(s)
for i in range(len(s)):  # 對每一個進行匹配
    t = 0
    for j in range(len(s[i])):  # 判斷全部同時都存在其餘中 且位置相同
        for i2 in [x for x in range(len(s2)) if s2[x] != s[i]]:  # 其餘 只比對有效
            flag = False
            for j2 in range(len(s2[i2])):
                if s[i][j] == s2[i2][j2] and j == j2:
                    t = t + 1
                    flag = True
                    break
            if flag:
                break
    if t == len(s[i]):
        del_row.append(i)
        s2.remove(s[i])

return [cp[i] for i in range(len(cp)) if i not in del_row]

網上的例子是用的index函數。在我寫過程當中,發現這裏有個坑。好比list中存在相同元素,就始終返回前一個匹配的索引,結果就會有問題。我就徹底避免了index函數。不知道哪一個是對的,目前知足使用須要,將就着用了。有點小尷尬。

Case

BaseCase斷言:

def checkFlag(self, p, r):
        """預期,實際"""
        err = str([p['url'], p['body'], r.text])
        try:
            b = False
            if (r.json()['flag'] in [1, '1', '', None, 'statistic_by_result', 0,
                                     "0", 'struct_product', 'v_select_jz_single']
                    or r.json()['message'] in ("暫無數據", "未查詢到數據")):
                b = True
            self.assertEqual(True, b, msg=err)
        except (json.JSONDecodeError, KeyError):  # 1.返回的不是json,好比下載、404  2.無flag
            self.assertEqual(200, r.status_code, msg=err)

最簡單的一個測試用例:

from case.PyPlatform2_0_2.Public import *

class Home(BaseCase):
    """首頁"""

    def setUp(self):
        log(testname(self.__repr__()) + '\n')
        record([testname(self.__repr__())])

    def test(self):
        """xxx"""
        self.req({
            "url": full_url("xxx"),
            "body": {}
        })

setup,輸出日誌。

Token

由於公司登錄用的token,跟cookie相似,保留登錄狀態,避免重複登錄。

如何處理token也是框架設計的一個要點。

1557471452464

Env設置token,由於每一個系統的登錄參數值都不同。

Var.token = login.get_token()

BaseCase.req在每次請求時獲取token,從而免登陸。

if "headers" not in p.keys():
    p['headers'] = {'token': ''}
    p['headers']['token'] = Var.token

CSV

寫文件:

if not os.path.exists(path):
    f = open(path, 'a', newline='')
    a = csv.writer(f)
    a.writerow(title)
    f.close()

f = open(path, 'a', newline='')
a = csv.writer(f)
try:
    a.writerow(d)
except UnicodeEncodeError:
    d[4] = "Unicode隱藏"  # response
    a.writerow(d)
f.close()

if get_file_size(path) >= 50 * 1024 * 1024:  # 超過50M刪除文件
    os.remove(path)
    record(title)

traceback自動生成文件名:

def _sys_name():
    t = str(traceback.extract_stack())
    b = True
    for x in os.listdir(case_dir):
        if x not in ["BaseCase.py", "__pycache__"]:
            if x in t:
                return x + "接口調用記錄" + current_date() + ".csv"
    if b:
        print("request找不到sysname")
        print(t)

HTMLTestRunner

根據通用的版本,也是參考網上一些現有的美化代碼,綜合了一下,根據本身需求作了改造。

近20交易日測試經過率

1552449137641

加了一個echarts,把最近20交易日的測試經過率,經過折線走勢圖的方式展現出來。監測系統穩定性。

數據存放和讀取在data目錄的csv文件中。

統計表格

1552449314757

按項目進行分組統計,增長測試說明一列,按顏色區別測試結果狀態,可點擊查看詳細描述和錯誤信息。

同時優化了總體的樣式效果。

排序:

# 按照經過率從小到大排序
passrate_value = []
for key in passrate:
    if key != 'total':
        passrate_value.append(float(passrate[key].replace('%', '')))

passrate_value.sort()

保存摺線圖數據:

today = datetime.datetime.now().strftime('%Y-%m-%d')
if '--' not in names:  # 跑單個系統不存
    if dao_is_trade_date(today):  # 非交易日不存
        with open(self.rct20_path, "r") as f:  # 讀取數據
            lines = csv.reader(f)
            lines = list(lines)
            for lin in lines:
                lin[0] = lin[0].replace('月', '-')
                lin[0] = lin[0].replace('日', '')
        rct_data = lines
        # print(rct_data)

        nowdate = datetime.datetime.now().strftime('%m-%d')
        # 若是有重複日期,先刪
        l = len(rct_data)
        while l != 0 and nowdate == rct_data[l - 1][0]:
            rct_data.pop(l - 1)
            l = len(rct_data)

        for pt in self.passrate_tl:
            n = pt[0]
            v = pt[1]
            row = []
            row.append(str(nowdate))
            row.append(str(n))
            row.append(str(v).replace('%', ''))
            rct_data.append(row)
        # 只存近20條
        row_20 = len(names) * 20
        if len(rct_data) > row_20:  # 超過20條
            for i in range(0, len(names)):
                rct_data.pop(0)

        with open(self.rct20_path, 'w', newline='') as f:
            writer = csv.writer(f)
            writer.writerows(rct_data)

拼接折線圖數據用於展現:

while ri < len(rct_data):  # 遍歷 ->s_data
    scan = []
    while ri < len(rct_data) and rct_data[ri][0] == trade_date[di]:
        s_data[rct_data[ri][1]].append(rct_data[ri][2])
        scan.append(rct_data[ri][1])
        ri += 1
    chg = list(set(names) ^ set(scan))  # 差集
    for c in chg:
        s_data[c].append('--')  # 增長/減小的項目,爲'--'
    di += 1

series = []  # 系列序列
s_names = s_data.keys()
for k in s_names:
    s = {}  # 單個系列
    s['name'] = k
    s['type'] = 'line'
    if s_data != {}:
        s['data'] = s_data[k]
    series.append(s)

這部分代碼是好久以前寫的了,代碼應該是不夠簡潔、高效、規範滴,是能夠優化滴。偷了懶沒有重構了。

用例設計

測試類型 描述
冒煙測試 全部接口寫單獨的test,確保調用正常。
全選測試 將全部參數儘量多的全選上,調用接口。
必定程序上能夠彌補結對測試的不足。
結對測試 如前文所述,關注兩兩組合的狀況。

參數值,部分採用隨機數。也視需求,從數據庫或其餘接口獲取數據。

結束語

第一次寫技術博客。

立刻工做5年。

算是一個嘗試吧。

版權申明:本文爲博主原創文章,轉載請保留原文連接及做者。

相關文章
相關標籤/搜索