『居善地』介面測試 — 7、介面自動化測試框架的設計與實現

繁華似錦Fighting 發表於 2021-06-05

(一)介面測試框架的思想

自動化測試框架不是一個模式,而是一種思想和方法的集合,通俗的講就是一個架構。

為了更好的瞭解自動化測試框架,應該對以下幾種自動化測試框架思想有一定的認知:

  • 模組化思想
  • 庫思想
  • 資料驅動思想
  • 關鍵字驅動思想

以上僅僅是代表了一種自動化測試的思想,並不能定義為框架。

上面講到框架=思想+方法,於是演化了以下五種框架:

1、模組化測試指令碼框架

需要建立小而獨立的可以描述的模組、片斷以及待測應用程式的指令碼。

這些樹狀結構的小指令碼組合起來,就能組成能用於特定的測試用例的指令碼。

2、測試庫框架

與模組化測試指令碼框架很類似,並且具有同樣的優點。

不同的是測試庫框架把待測應用程式分解為過程和函式而不是指令碼。

框架需要建立描述模組、片斷以及待測應用程式的功能庫檔案。

3、關鍵字驅動或表驅動的測試框架

框架需要開發資料表和關鍵字。

這些資料表和關鍵字獨立於執行它們的測試自動化工具,並可以用來“驅動"待測應用程式和資料的測試指令碼程式碼,關鍵宇驅動測試看上去與手工測試用例很類似。

在一個關鍵字驅動測試中,把待測應用程式的功能和每個測試的執行步驟一起寫到一個表中。

測試框架可以通過很少的程式碼來產生大量的測試用例。

同樣的程式碼在用資料表來產生各個測試用例的同時被複用。

4、資料驅動測試框架

在這裡測試的輸入和輸出資料是從資料檔案中讀取(資料池,ODBC源,CSV檔案,EXCEL檔案,Json檔案,Yaml檔案,ADO物件等)並且通過捕獲工具生成或者手工生成的程式碼指令碼被載入到變數中。

在這個框架中,變數不僅被用來存放輸入值還被用來存放輸出的驗證值。

整個程式中,測試指令碼來讀取數值檔案,記載測試狀態和資訊。這類似於表驅動測試,在表驅動測試中,它的測試用例是包含在資料檔案而不是在指令碼中,對於資料而言,指令碼僅僅是一個“驅動器”,或者是一個傳送機構。

然而資料驅動測試不同於表驅動測試,儘管導航資料並不包含在表結構中。

在資料驅動測試中,資料檔案中只包含測試資料。

5、混合測試自動化框架

最普遍的執行框架是上面介紹的所有技術的一個結合,取其長處,彌補其不足。

混合測試框架是由大部分框架隨著時間並經過若干專案演化而來的。

(二)介面測試框架結構解析

  • common目錄:一些公共方法存放目錄。
    • 封裝請求
      send_method.py:封裝介面請求方法。
    • 封裝獲取返回值
      getKeyword_forResult.py:通過關鍵字獲取介面返回值。
    • 讀取資料方法
  • interface目錄:存放介面的目錄。
    每一個介面或者一類介面來寫一個interface(也就是一個介面對應一個.py檔案)
    • 對該介面的請求:用於單介面測試
    • 根據業務獲取介面返回值:用於關聯介面測試
  • script目錄:存放測試用例的目錄。
    也可以命令為testCase目錄。
    介面測試用例包括:
    • 單介面測試用例
    • 關聯介面測試用例
  • Config目錄:存放配置檔案。配置一些常量,例如資料庫的相關資訊,介面的相關資訊等。
  • Data目錄:存放公共部分資料,比如測試資料,excel檔案等等。
  • Log目錄:存放logging日誌資訊。
  • Reports目錄:存放介面測試報告目錄。
  • runMain.py檔案:主程式入檔案口,用於執行case。

(三)介面自動化測試框架封裝實現

之前分析完了介面測試框架的設計與架構,下面我們就來一步一步的完成介面自動化測試框架的實現。

1、建立測試框架專案

Student Management System Interface testing framework建立一個測試專案SMSITF

專案名上右鍵 —> New —> Python Package —> 建立common目錄。

同理建立如下目錄:

  • interface目錄:存放介面的目錄。
  • script目錄:存放測試用例的目錄。
  • Config目錄:存放配置檔案。配置一些常量,例如資料庫的相關資訊,介面的相關資訊等。
  • Data目錄:存放公共部分資料,比如測試資料,excel檔案等等。
  • Log目錄:存放logging日誌資訊。
  • Reports目錄:存放介面測試報告目錄。

建立好後如下圖:

image

接下來我們要一步一步實現這個框架裡邊的功能。

DictionaryPython Package目錄說明:

Dictionary在Pycharm中就是一個資料夾,放置資原始檔,該資料夾其中並不包含__init.py__檔案。

Python Package資料夾會自動建立__init.py__檔案,換句話說Python Package就是建立一個目錄,其中包括一組模組和一個__init.py__檔案。

2、封裝傳送請求方法

一些公共的方法,要寫在common目錄中,主要是封裝使用Requests庫傳送請求的方法。

其他所有的公共的方法都可以封裝在common目錄中。

"""
send_method.py 檔案說明:
1,封裝介面請求方式
    根據專案介面文件提供的內容進行封裝
    不同的專案,sendmethod也不太一樣,如請求體格式等。
2.封裝思路-結合介面三要素
    請求方式+請求地址
    請求引數
    返回值
3.以學生管理系統SMS為例:
    結合學生管理系統專案的介面文件,封裝SendMethod類

"""
# 匯入所需模組
import requests
import json


# 封裝請求模組
class SendMethod:
    """
        結合學生管理系統SMS,請求方式包括如下:
            get ---> parmas標準請求引數
            post--->請求引數型別 json
            put --->請求引數型別 json
            delete ---> parmas標準請求引數
    """

    # 定義該方法為靜態方法
    @staticmethod
    def send_method(method, url, parmas=None, json=None):
        """
        封裝適用於學生管理系統專案的介面請求
        :param method: 請求方式
        :param url: 請求地址
        :param parmas: get和delete請求引數
        :param json: post和put請求引數
        :param headers: 請求頭
        :return:
        """
        # 定義傳送請求的方法
        if method == "get" or method == "delete":
            response = requests.request(method=method, url=url, params=parmas)
        elif method == "post" or method == "put":
            response = requests.request(method=method, url=url, json=json)
            # 如果有不同的請求頭,還可以繼續新增接收的引數
            # response = requests.request(method=method, url=url, json=json, data=data, files=data)
        else:
            # 這裡是簡單處理,完成封裝需要加上異常處理。
            response = None
            print("請求方式不正確")

        # 如果請求方式是delete,只返回狀態碼
        # 這是根據專案介面文件中delete方法的返回規則定的。
        if method == "delete":
            return response.status_code
        else:
            # 專案中介面的返回值是json格式的,就可以用json()進行格式化返回結果。
            return response.json()

    @staticmethod
    def json_2_python(res):
        """
        格式化返回資料
        :param res:介面返回的資料
        :return:
        """
        return json.dumps(res, indent=2, ensure_ascii=False)


if __name__ == '__main__':
    method = "post"
    url = "http://127.0.0.1:8000/api/departments/"
    data = {
        "data": [
            {
                "dep_id": "T02",
                "dep_name": "介面測試學院",
                "master_name": "Test-Master",
                "slogan": "Here is Slogan"
            }
        ]
    }
    res = SendMethod.send_method(method=method, url=url, json=data)
    # print(res)
    print(SendMethod.json_2_python(res))

    # method = "get"
    # params = {"$dep_id_list": "1, 2, 3"}
    # res = SendMethod.send_method(method=method, url=url, json=data)
    # print(SendMethod.json_2_python(res))

3、封裝獲取介面返回結果指定內容

該檔案是封裝處理返回值結果的一些方法。

我們需要用到一個Python中的模組JsonPath ,下面就先來介紹一下JsonPath 模組。

(1)JsonPath 介紹

用來解析多層巢狀的Json資料。

JsonPath是一種資訊抽取類庫,是從JSON文件中抽取指定資訊的工具,提供多種語言實現版本,包括:JavascriptPythonPHPJava

JsonPath 對於 JSON 來說,相當於 XPath 對於 XML。

(2)JsonPath 安裝

安裝方法:pip install jsonpath

使用方法如下:

# 匯入jsonpath模組
import jsonpath模組

# 巢狀n層也能取到所有key_nane資訊,
# 其中:"$"表示最外層的{},
# ".."表示模糊匹配,
# 當傳入不存在的key_nane時,程式會返回false。

res = jsonpath.jsonpath(response, f"$..{keyword}")[0]

"""
jsonpath方法說明
jsonpath(obj, expr, result_type='VALUE', debug=0, use_eval=True):

# obj表是要處理的json物件。
# expr是jsonpath匹配表示式。$..{keyword} 這種方式比較通用
"""

JsonPath官方文件:http://goessner.net/articles/JsonPath

github上有它的應用:https://github.com/json-path/JsonPath(Java中的JsonPath使用文件)

(3)JsonPath與XPath語法對比

Json結構清晰,可讀性高,複雜度低,非常容易匹配,下表中對應了XPath的用法。

XPath JSONPath 描述
/ $ 根節點
. @ 現行節點
/ .or[] 取子節點
.. n/a 取父節點,Jsonpath未支援
// .. 就是不管位置,選擇所有符合條件的條件
* * 匹配所有元素節點
@ n/a 根據屬性訪問,Json不支援,因為Json是個Key-value遞迴結構,不需要屬性訪問。
[] [] 迭代器標示(可以在裡邊做簡單的迭代操作,如陣列下標,根據內容選值等)
| [,] 支援迭代器中做多選。
[] ?() 支援過濾操作.
n/a () 支援表示式計算
() n/a 分組,JsonPath不支援

(4)getKeyword_forResult.py檔案實現

"""
getKeyword_forResult.py檔案說明:
1.作用
    在介面返回值中,通過關鍵獲取獲取對應欄位內容
2,前提:需要安裝一個庫:jsonpath庫
    安裝jsonpath : pip install jsonpath
    使用jsonpath模組進行處理更加方便

"""
# 匯入jsonpath模組
import jsonpath


# 封裝獲取介面返回值方法
class GetKeyword:
    # 定義成一個靜態方法
    @staticmethod
    def get_keyword(response: dict, keyword):
        """
        通過關鍵字獲取對應返回值,如果有多個值,只返回第一個,
        如果關鍵字不存在,返回False。
        :param:response 資料來源  字典格式
        :param:keyword 要獲取的欄位
        :return:
        """
        try:
            return jsonpath.jsonpath(response, f"$..{keyword}")[0]
        except:
            print("關鍵字不存在")

    @staticmethod
    def get_keywords(response: dict, keyword):
        """
        通過關鍵字獲取一組資料
        :param response: 資料來源 dict格式
        :param keyword:  如果關鍵字不存在,返回False
        :return:
        """
        try:
            return jsonpath.jsonpath(response, f"$..{keyword}")
        except:
            print("關鍵字不存在")


if __name__ == '__main__':
    response = {
        "count": 2,
        "next": "下一頁",
        "previous": None,
        "results": [
            {
                "dep_id": "10",
                "dep_name": "tester_10",
                "master_name": "master_10",
                "slogan": "隨便"
            },
            {
                "dep_id": "11",
                "dep_name": "tester_11",
                "master_name": "master_11",
                "slogan": "隨便"
            }
        ]
    }
    keyword = "dep_id"
    # print(GetKeyword.get_keyword(response, keyword))
    print(GetKeyword.get_keywords(response, keyword))

4、介面目錄中的方法的實現

每一個介面或者一類介面封裝成一個interface(也就是一個介面對應一個.py檔案)

  • 對該介面的請求:用於單介面測試。
  • 根據業務獲取介面返回值:用於關聯介面測試。

(關於一個介面,所對應要測試哪幾個方面的業務,都封裝到該檔案中,會用到上面commn目錄中封裝好的公共方法)

示例如下:

(1)示例1:封裝新增學院介面

"""
新增學院介面
1.單介面測試方法
2.關聯介面測試方法
    獲取返回值中的欄位
"""
# 匯入自定義的公共方法
from common.send_method import SendMethod
from common.getKeyword_forResult import GetKeyword


# 封裝新增學院介面測試
class Add_department:

    # url和請求方式對於一個介面來說是固定的,
    # 所以這兩個引數可以寫在初始化方法中。
    def __init__(self, url, method="post"):
        self.method = method
        self.url = url

    def add_dep(self, data):
        """
        定義新增學院介面:針對單介面測試
        :param data: 新增學院的請求引數
        :return:
        """
        return SendMethod.send_method(self.method, url=self.url, json=data)

    def get_keyword(self, data, keyword):
        """
        獲取新增成功後的關鍵字值:為關聯介面測試準備
        :param data:
        :param keyword:
        :return:
        """
        res = self.add_dep(data)

        # 獲取新增學院介面返回值中的學院的具體某一屬性
        return GetKeyword.get_keyword(res, keyword)


if __name__ == '__main__':
    url = "http://127.0.0.1:8000/api/departments/"
    data = {
        "data": [
            {
                "dep_id": "T03",
                "dep_name": "Test學院",
                "master_name": "Test-Master",
                "slogan": "Here is Slogan"
            }
        ]
    }
    add = Add_department(url)
    res = add.add_dep(data)  # 新增學院介面方法
    print(res)
    keyword = "dep_id"
    dep_id = add.get_keyword(data, keyword)  # 獲取新增成功後depid
    print(dep_id)

(2)示例2:封裝查詢學院介面

"""
get_dep.py檔案說明:
1.查詢介面測試
2.獲取查詢介面返回值
"""
from common.send_method import SendMethod


class Get_Departments:
    def __init__(self, url, method="get"):
        self.url = url
        self.method = method

    def get_departments(self):
        """
        查詢所有學院
        :return:
        """
        return SendMethod.send_method(method=self.method, url=self.url)

    def get_department(self, dep_id):
        """
        根據id查詢單個學院
        :return:
        """
        url = self.url + f"{dep_id}/"
        return SendMethod.send_method(method=self.method, url=url)

    def get_department_for_multpart(self, data):
        """
        根據引數查詢學院
        :return:
        """
        return SendMethod.send_method(method=self.method, url=self.url, parmas=data)


if __name__ == '__main__':
    url_1 = "http://127.0.0.1:8000/api/departments/"
    data = {"$dep_id_list": "12,13"}
    get_dep = Get_Departments(url=url_1)
    # 查詢所有學院
    # print(get_dep.get_departments())
    # 查詢指定學院
    dep_id = 16
    # print(get_dep.get_department(dep_id))
    # 根據條件查詢學院
    print(get_dep.get_department_for_multpart(data))

5、測試用例目錄的實現

script目錄中存放的是測試用例,包括單介面和組合介面的測試用例。

測試用例是在unittest框架下編寫,用法同UI測試框架。

(1)編寫單介面測試用例

"""
測試新增學院介面
"""

# 測試用例是在unittest框架下編寫
import unittest
from interface.add_departments import Add_department  # 匯入新增學院介面
from common.getKeyword_forResult import GetKeyword  # 返回值處理介面


# 測試新增和查詢學院的關聯型介面
class Test_Add_Dep(unittest.TestCase):
    def setUp(self) -> None:
        self.url = "http://127.0.0.1:8000/api/departments/"
        # 例項化Add_department
        self.add_dep = Add_department(self.url)

    # 開始編寫測試用例
    def test_add_dep_success(self):
        """
        測試新增學院成功介面
        :return:
        """

        # 封裝請求引數
        data = {
            "data": [
                {
                    "dep_id": "T100",
                    "dep_name": "Test學院",
                    "master_name": "Test-Master",
                    "slogan": "Here is Slogan"
                }
            ]
        }

        # 新增學院
        response = self.add_dep.add_dep(data)
        # 獲取新增成功後的dep.id
        """
        # 因為直接使用該方法相當於又執行了一次新增學院介面
        # 所以不能夠這樣呼叫
        self.add_dep.get_depid(data)
        """
        res_dep_id = GetKeyword.get_keyword(response["create_success"], "dep_id")
        expect = "T100"
        self.assertEqual(res_dep_id, expect)

    # 測試新增學院完整性實現
    def test_add_dep(self):
        """
        測試新增學院介面
        :return:
        """

        # 封裝請求引數
        data = {
            "data": [
                {
                    "dep_id": "T101",
                    "dep_name": "Test學院",
                    "master_name": "Test-Master",
                    "slogan": "Here is Slogan"
                }
            ]
        }
        # 新增學院
        response = self.add_dep.add_dep(data)

        """
        並返回值的驗證有3種情況
            #1.新增成功
            #2.添川id已存在的學院
            #3.參敖錯誤(自己實現)
        根據對介面檔的分析
            可以通過判斷返回值是否包含“status_code”區分1,2和3,然後區分1,2
            根據返回值中already_exist.count是否為0,判斷是否新增成功
        """

        # 這裡只判斷上面的1,2情況,工作中根據實際業務自己在完成
        if GetKeyword.get_keyword(response["already_exist"], "count") == 0:
            # 獲取新增成功後的dep.id
            res_dep_id = GetKeyword.get_keyword(response["create_success"], "dep_id")
        else:
            res_dep_id = GetKeyword.get_keyword(response["already_exist"], "dep_id")

        expect = "T101"
        self.assertEqual(res_dep_id, expect)


if __name__ == '__main__':
    unittest.main()

(2)編寫組合介面測試用例

"""
測試新增和查詢介面(組合介面業務)
    先新增--->再查詢
"""
# 測試用例是在unittest框架下編寫
import unittest
from interface.add_departments import Add_department  # 匯入新增學院介面
from interface.get_departments import Get_Departments  # 查詢學院介面
from common.getKeyword_forResult import GetKeyword  # 返回值處理介面


# 測試新增和查詢學院的關聯型介面
class Test_Add_Get_Dep(unittest.TestCase):
    def setUp(self) -> None:
        self.url = "http://127.0.0.1:8000/api/departments/"
        # 例項化Add_department新增學院
        self.add_dep = Add_department(self.url)
        # 例項化Get_Departments查詢學院
        self.get_dep = Get_Departments(self.url)

    # 開始編寫測試用例
    def test_add_get(self):
        # 封裝請求引數
        add_data = {
            "data": [
                {
                    "dep_id": "T03",
                    "dep_name": "Test學院",
                    "master_name": "Test-Master",
                    "slogan": "Here is Slogan"
                }
            ]
        }

        # 一下邏輯待查證,知道組合的形式即可。
        # 執行新增學院介面。目的:獲取新增成功後的學院id
        # 獲取新增學院後的id
        dep_id = self.add_dep.get_keyword(add_data, "dep_id")
        # 查詢新增學院資訊
        result = self.get_dep.get_department(dep_id)
        # 通過獲取查詢後的學院id作為實際結果
        res_dep_id = GetKeyword.get_keyword(result, "dep_id")
        # 獲取預期結果id
        expect = GetKeyword.get_keyword(add_data, "dep_id")
        # 斷言結果
        self.assertEqual(expect, res_dep_id)


if __name__ == '__main__':
    unittest.main()


6、測試用例引數化實現

(1)準備資料

先建立一個Excel表格,裡邊填寫如下資料

dep_id dep_nane master_nane slogan expect
T1001 學院1001 tester_1001 slogan1001 T1001
學院1002 tester_1002 slogan1002 400
T1003 tester_1003 slogan1003 400
T1004 學院1004 slogan1004 400
T1005 學院1005 tester_1005 T1005

把Excel表格中的資料準備好之後,放入專案的data目錄中即可。注意要把Excel表格儲存為.xls格式,相容性好。

(2)在common目錄中編寫讀取Excel資料的指令碼

編寫opreation_excel.py指令碼如下:

import xlrd
from xlrd import xldate_as_tuple
from datetime import datetime

class OperationExcel:
    def __init__(self, filepath):
        book = xlrd.open_workbook(filename=filepath)
        self.sheet = book.sheet_by_index(0)

    def read_excel(self):
        rows = self.sheet.nrows
        cols = self.sheet.ncols
        all_data_list = []
        for row in range(1, rows):
            data_list = []
            for col in range(cols):
                ctype = self.sheet.cell(row, col).ctype
                cell = self.sheet.cell_value(row, col)
                if ctype == 2 and cell % 1 == 0:
                    cell = int(cell)
                elif ctype == 3:
                    date = datetime(*xldate_as_tuple(cell, 0))
                    cell = date.strftime("%Y-%m-d %H-%M-%S")
                elif ctype == 4:
                    cell = True if cell == 1 else False  # 三目雲演算法
                data_list.append(cell)
            all_data_list.append(data_list)
        return all_data_list

    def get_data_by_dict(self):
        keys = self.sheet.row_values(0)
        values = self.read_excel()
        data_list = []
        for value in values:
            tmp = zip(keys, value)
            data_list.append(dict(tmp))
        return data_list


if __name__ == '__main__':
    oper = OperationExcel('testdata.xlsx')
    # data = oper.read_excel()
    data = oper.get_data_by_dict()
    print(data)

(3)在script目錄中編寫測試用例

在script目錄中編寫test_add_dep_batch.py測試用例。

"""
新增學院介面測試--批量新增
"""

# 測試用例是在unittest框架下編寫
import unittest
from interface.add_departments import Add_department  # 匯入新增學院介面
from common.getKeyword_forResult import GetKeyword  # 返回值處理介面
# 步驟1:匯入OperationExcel資料讀取指令碼和ddt模組
from common.opreationexcel import OperationExcel
import ddt

# 步驟2:對OperationExcel進行例項化
# 獲得檔案物件
oper = OperationExcel("../data/add_dep.xls")
# 獲取資料
test_data = oper.get_data_by_dict()


# 測試新增和查詢學院的關聯型介面
# 步驟3
@ddt.ddt()
class Test_Add_Dep(unittest.TestCase):
    def setUp(self) -> None:
        self.url = "http://127.0.0.1:8000/api/departments/"
        # 例項化Add_department
        self.add_dep = Add_department(self.url)

    # 開始編寫測試用例
    # 步驟4
    @ddt.data(*test_data)
    def test_add_dep_success(self, data):  # 步驟5:出入data引數
        """
        測試新增學院成功介面
        :return:
        """
        # 步驟6:準備資料
        req_data = {
            "data": [
                {
                    "dep_id": data["dep_id"],
                    "dep_name": data["dep_name"],
                    "master_name": data["master_name"],
                    "slogan": data["slogan"]
                }
            ]
        }

        # 新增學院
        response = self.add_dep.add_dep(req_data)
        # 獲取新增成功後的dep.id

        # 步驟7:完成測試邏輯
        # 如果新增學院引數請求錯誤,會出現status_code屬性
        # 且status_code屬性返回400
        if "status_code" in response.keys():
            res = GetKeyword.get_keyword(response, "status_code")
        else:
            # 新增學院成功,則獲取新增後學院的id
            """
            # 因為直接使用該方法相當於又執行了一次新增學院介面
            # 所以不能夠這樣呼叫
            self.add_dep.get_depid(data)
            """
            res = GetKeyword.get_keyword(response["create_success"], "dep_id")

        # 斷言
        self.assertEqual(res, data["expect"])


if __name__ == '__main__':
    unittest.main()

以上就完成了一個最簡單,最基礎的介面自動化測試框架的搭建。