引子
數據寫在代碼裏,追求快速編寫用例,是我設計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中也能體驗到測試平臺的順暢感。大家也可以試一下。
第一步,添加用例:
第二步,添加步驟:
Step這一行,從左到右順序錄入,步驟名稱,步驟函數,cache,特別順手。
第三步,添加步驟函數,直接複製這裏的函數名,到steps包下面新建文件:
然後輸入函數定義:
這裏一定要記得輸入TepCache的Typing提示,以獲得PyCharm語法提示:
接着順序輸入url、headers、body:
第四步,在data目錄下新建數據文件:
第五步,回到步驟函數,做參數化、請求、斷言、數據提取等:
第六步,再回到測試用例,導入步驟函數:
其他步驟以此類推。tep後續將以MVC分層設計編寫方式爲主,老用例仍然會兼容,可以不修改,新用例可以在tep正式發佈後,嘗試下。
tep-template加了幾個新庫可能需要安裝下:
pip install jsonpath
pip install filelock
pip install pytest-xdist
目前代碼已經上傳到預覽版,歡迎加我或進羣交流。
參考資料: