當Pytest遇上MVC分層設計自動化用例就該這麼寫

引子

數據寫在代碼裏,追求快速編寫用例,是我設計tep的一個特點,這在個人編寫時是一種非常良好的體驗。但相比於HttpRunner、JMeter等來說,總覺得還差點意思。思考良久,總結爲三個字:工程化。工程化是我近一年在學習Java並參與了2個測試平臺模塊開發,和寫了幾個小工具後,感受到的一種編程思想。而其中最明顯的就是Spring的MVC分層設計。爲了讓tep更工程化,後續版本將以MVC模塊編寫用例爲準,同時會兼容之前的腳本式快速編寫。

示例

目錄結構

測試用例都放在一個文件夾下:

test_case:用例主程序;

steps:測試步驟;

data:純粹的json;

測試用例

test_case.py是測試用例,包含的只有測試步驟:

import allure

from examples.tests.LoginToPayFlow.steps.step_add_cart import step_add_cart
from examples.tests.LoginToPayFlow.steps.step_order import step_order
from examples.tests.LoginToPayFlow.steps.step_pay import step_pay
from examples.tests.LoginToPayFlow.steps.step_search_sku import step_search_sku
from utils.cache import TepCache
from utils.step import Step

"""
測試登錄到下單流程,需要先運行utils/fastapi_mock.py
"""


@allure.title("從登錄到下單支付")
def test(login, env_vars, case_vars):
    case_vars.put("token", login["token"])
    cache = TepCache(env_vars=env_vars, case_vars=case_vars)

    Step("搜索商品", step_search_sku, cache)
    Step("添加購物車", step_add_cart, cache)
    Step("下單", step_order, cache)
    Step("支付", step_pay, cache)

TepCache是“緩存”,包括global_vars、env_vars、case_vars三個級別的變量池。

Step是一個泛化調用類,作用是打日誌,調用第二個參數對應的步驟函數。

測試步驟

from utils.cache import TepCache
from utils.func import data
from utils.http_client import request


def step_search_sku(cache: TepCache):
    url = cache.env_vars["domain"] + "/searchSku"
    headers = {"token": cache.case_vars.get("token")}
    body = data("查詢SKU.json")

    response = request("get", url=url, headers=headers, params=body)
    assert response.status_code < 400

    cache.case_vars.put("skuId", response.jsonpath("$.skuId"))
    cache.case_vars.put("skuPrice", response.jsonpath("$.price"))
from utils.cache import TepCache
from utils.func import data
from utils.http_client import request


def step_add_cart(cache: TepCache):
    url = cache.env_vars["domain"] + "/addCart"
    headers = {"token": cache.case_vars.get("token")}
    body = data("添加購物車.json")
    body["skuId"] = cache.case_vars.get("skuId")

    response = request("post", url=url, headers=headers, json=body)
    assert response.status_code < 400

    cache.case_vars.put("skuNum", response.jsonpath("$.skuNum"))
    cache.case_vars.put("totalPrice", response.jsonpath("$.totalPrice"))

步驟函數以step開頭,尤其注意的是cache藉助Python Typing提示,可以在編寫下面代碼時,獲得PyCharm語法提示,所以一定不要忘了加上: TepCache

步驟函數裏面由基本信息(url、headers、body),數據初始化,請求,斷言,數據提取幾個部分組織,從上往下順序編寫

測試數據

數據代碼分離,在MVC分層設計中這點就特別重要,在data目錄下存放的不做任何參數化的純粹json:

參數化都放在步驟函數裏面來寫。數據代碼分離的好處是,比如現在寫的用例是買3件商品,假如你想改成買10件,只改json的數據就可以了,不需要改動任何代碼。你可能會想,把這個數字放在代碼裏,不也是隻改個值嗎?確實如此,但這不符合MVC分層設計了。

以SpringMVC作爲參照:

testcase.py相當於controller,steps相當於service,data相當於pojo,各層只做自己的事,多寫點代碼,換來的是可讀性強、維護性高、層次分明的“工程化資產”。

日誌輸出

原理

Step泛化調用:

from loguru import logger

from utils.cache import TepCache


class Step:
    """
    測試步驟,泛化調用
    """

    def __init__(self, name: str, action, cache: TepCache):
        logger.info("----------------" + name + "----------------")
        action(cache)

TepCache緩存:

class TepCache:
    """
    提供緩存服務,包括全局變量、環境變量、用例變量
    """
    def __init__(self, global_vars=None, env_vars=None, case_vars=None):
        self.global_vars = global_vars
        self.env_vars = env_vars
        self.case_vars = case_vars

fixture實現的變量池:

@pytest.fixture(scope="session")
def global_vars():
    """
    全局變量,讀取resources/global_vars.yaml,返回字典
    """
    with open(os.path.join(Config.project_root_dir, "resources", "global_vars.yaml")) as f:
        return yaml.load(f.read(), Loader=yaml.FullLoader)


@pytest.fixture(scope="session")
def env_vars():
    """
    環境變量,讀取resources/env_vars下的變量模板,返回字典
    """
    env_active = tep_config()['env']["active"]
    env_filename = f"env_vars_{env_active}.yaml"
    with open(os.path.join(Config.project_root_dir, "resources", "env_vars", env_filename)) as f:
        return yaml.load(f.read(), Loader=yaml.FullLoader)


@pytest.fixture(scope="session")
def case_vars():
    """
    測試用例的動態變量,1條測試用例1個實例,彼此隔離
    """

    class CaseVars:
        def __init__(self):
            self.dict_in_memory = {}

        def put(self, key, value):
            self.dict_in_memory[key] = value

        def get(self, key):
            value = ""
            try:
                value = self.dict_in_memory[key]
            except KeyError:
                logger.error(f"獲取用例變量的key不存在,返回空串: {key}")
            return value

    return CaseVars()

登錄fixture:

import pytest
from loguru import logger

from utils.http_client import request


@pytest.fixture(scope="session")
def login(tep_context_manager, env_vars):
    """
    tep_context_manager是爲了兼容pytest-xdist分佈式執行的上下文管理器
    該login只會在整個運行期間執行一次
    """

    def produce_expensive_data(variable):
        logger.info("----------------開始登錄----------------")
        response = request(
            "post",
            url=variable["domain"] + "/login",
            headers={"Content-Type": "application/json"},
            json={"username": "dongfanger", "password": "123456"}
        )
        assert response.status_code < 400
        logger.info("----------------登錄成功----------------")
        return response.json()

    return tep_context_manager(produce_expensive_data, env_vars)

TepResponse支持response.jsonpath寫法:

def request(method, url, **kwargs):
    template = """\n
Request URL: {}
Request Method: {}
Request Headers: {}
Request Payload: {}
Status Code: {}
Response: {}
Elapsed: {}
"""
    start = time.process_time()
    response = requests.request(method, url, **kwargs)  # requests.request原生用法
    end = time.process_time()
    elapsed = str(decimal.Decimal("%.3f" % float(end - start))) + "s"
    headers = kwargs.get("headers", {})
    kwargs.pop("headers")
    payload = kwargs
    log = template.format(url, method, json.dumps(headers), json.dumps(payload), response.status_code, response.text,
                          elapsed)
    logger.info(log)
    allure.attach(log, f'request & response', allure.attachment_type.TEXT)
    return TepResponse(response)


class TepResponse(Response):
    """
    二次封裝requests.Response,添加額外方法
    """

    def __init__(self, response):
        super().__init__()
        for k, v in response.__dict__.items():
            self.__dict__[k] = v

    def jsonpath(self, expr):
        """
        此處強制取第一個值,便於簡單取值
        如果複雜取值,建議直接jsonpath原生用法
        """
        return jsonpath.jsonpath(self.json(), expr)[0]

讀取數據文件:

def data(relative_path: str) -> dict:
    """
    與steps同層級的data目錄+傳入的相對路徑
    """
    caller = inspect.stack()[1]
    steps_path = os.path.dirname(caller.filename)
    data_path = os.path.join(os.path.dirname(steps_path), "data", relative_path)
    if os.path.exists(data_path):
        with open(data_path, encoding="utf8") as f:
            return json.load(f)
    logger.error("數據文件不存在")
    return {}

體驗

本次設計的編寫方法,跟我公司的測試平臺的體驗很類似,因爲習慣了平臺操作,用這種方式寫代碼竟然出奇的習慣,基本上沒有卡點或特別繞的感覺,在PyCharm中也能體驗到測試平臺的順暢感。大家也可以試一下。

第一步,添加用例:

image-20221217042601078

第二步,添加步驟:

image-20221217042716815

Step這一行,從左到右順序錄入,步驟名稱,步驟函數,cache,特別順手。

第三步,添加步驟函數,直接複製這裏的函數名,到steps包下面新建文件:

image-20221217042909570

然後輸入函數定義:

image-20221217043003868

這裏一定要記得輸入TepCache的Typing提示,以獲得PyCharm語法提示:

image-20221217043118673

接着順序輸入url、headers、body:

image-20221217043309378

第四步,在data目錄下新建數據文件:

image-20221217043415876

第五步,回到步驟函數,做參數化、請求、斷言、數據提取等:

image-20221217043710960

第六步,再回到測試用例,導入步驟函數:

image-20221217043814278

其他步驟以此類推。tep後續將以MVC分層設計編寫方式爲主,老用例仍然會兼容,可以不修改,新用例可以在tep正式發佈後,嘗試下。

tep-template加了幾個新庫可能需要安裝下:

pip install jsonpath
pip install filelock
pip install pytest-xdist

目前代碼已經上傳到預覽版,歡迎加我或進羣交流。

參考資料:

tep預覽版 https://gitee.com/dongfanger/tep-template

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章