需求
- 在測試框架中,往往需要測試資料和程式碼分離,使用CSV或JSON等資料檔案儲存資料,使用程式碼編寫測試邏輯
- 一個用例過程往往可以測試多組資料,Pytest原生的引數化往往需要我們自己手動讀取資料檔案,比較麻煩又略顯混亂
- 我們如何能把資料檔案按約定的目錄和檔名存起來,檔案中可以存一組或多組資料,用例自動根據裡面的資料進行資料驅動呢?
- 另外,為了靈活,我們儘量可以支援多種資料型別,如YAML/JSON/INI/CSV等
- 是否能僅自動化發現測試資料,不啟用資料驅動
約定
- 我們約定,用例使用
case_data
這個fixture時,可以自動獲取到用例對應資料,如
# filename: test_a.py
def test_a_01(case_data):
print(case_data)
- 我們新增一個自定義引數和配置項 datapath,設定為測試資料的根目錄,新增一個ddt引數和配置項,設定是否啟用ddt模式,預設不啟用,如
# filename: pytest.ini
[pytest]
datapath=testdata
ddt=true
- 我們約定 測試用例(測試函式)的資料,存放在 測試資料的根目錄/測試檔名/目錄下 (暫時忽略模組),測試資料檔名(不包含字尾) 必須 與 測試函式名相同,如 test_a.py中test_a_01用例對應資料
testdata/
test_a/
test_a_01.yaml
- 測試資料檔案.yaml/.json/.ini/.csv任選一種,同時載入優先順序為 .yaml > .json > .ini > .csv
- ini檔案 分段名 如
[data1]
"為該資料的id標識,並支援自動繼承[DEFAULT]
段預設資料,如
# filename: test_a_01.ini
[DEFAULT]
age=15
[data1]
name=張三
[data2]
name=李四
age=16
[data3]
name=王五
age=17
- json或yaml檔案,如為字典格式,則每個key為該資料的id標識,如為列表格式,每組資料中如果有_id欄位,則視為資料id標識,如不包含_id,則資料無id標識,如
# filename: test_a_01.yaml
data1:
name: 張三
age: 15
data2:
name: 李四
age: 16
data3:
name: 張三
age: 17
或
# filename: test_a_01.yaml
- _id: data1
name: 張三
age: 15
- _id: data2
name: 李四
age: 16
- _id: data3
name: 張三
age: 17
- csv檔案第一行必須欄位標題行,如
name,age
,每組資料中如果有_id欄位,則視為資料id標識,如不包含_id,則資料無id標識,如
test_a_01.csv
_id,name,age
data1,張三,15
data2,李四,16
data3,王五,17
實現
實現思路:使用pytest hooks方法pytest_generate_tests來根據資料檔案內容動態生成用例
所需依賴:pip install pytest filez pyyaml
- 新增自定義配置項
# filename: conftest.py
def pytest_addoption(parser):
# 制定測試資料根目錄
parser.addoption("--datapath", action="store", help="testdata dir path")
parser.addini('datapath', help='testdata dir path')
# 是否啟用資料驅動
parser.addoption("--ddt", action="store_true", help="enable data driven test for testdata")
parser.addini('ddt', help='enable data driven test for testdata')
- 核心實現
# filename: conftest.py
def pytest_generate_tests(metafunc):
if "case_data" in metafunc.fixturenames:
config = metafunc.config
datapath = config.getoption("--datapath") or config.getini("datapath")
ddt = config.getoption("--ddt") or config.getini("ddt")
# 如果配置的datapath為絕對路徑,則直接視為測試資料根目錄
if datapath.startswith("/"):
testdata_dir = Path(datapath)
else:
# 如果不是絕對路徑則前面新增測試專案根目錄
testdata_dir = config.rootdir / datapath
# 用例檔名(不帶副檔名),如test_a
testfile_name = metafunc.definition.fspath.purebasename
# 用例函式名,如test_a_01
testcase_name = metafunc.definition.name
# 用例節點id,如test_a.py::test_a_01
testcase_node_id = metafunc.definition.nodeid
# 讀取對應資料檔案
testcase_csv_datafile = testdata_dir / testfile_name / f'{testcase_name}.csv'
testcase_ini_datafile = testdata_dir / testfile_name / f'{testcase_name}.ini'
testcase_json_datafile = testdata_dir / testfile_name / f'{testcase_name}.json'
testcase_yaml_datafile = testdata_dir / testfile_name / f'{testcase_name}.yaml'
if testcase_yaml_datafile.exists():
# 讀取yaml檔案
with open(testcase_yaml_datafile) as f:
file_data = yaml.safe_load(f)
elif testcase_json_datafile.exists():
# 讀取json檔案
file_data = file.load(testcase_json_datafile)
elif testcase_ini_datafile.exists():
# 讀取ini檔案
file_data = file.load(testcase_ini_datafile)
elif testcase_csv_datafile.exists():
# 讀取csv檔案-帶標題行
file_data = file.load(testcase_csv_datafile, header=True)
else:
return
# 對列表和字典格式資料進行處理
data, ids = None, None
if isinstance(file_data, list):
data = file_data
if len(file_data) > 0 and file_data[0].get('_id'):
ids = [item.get('_id') for item in file_data]
data = [item for item in file_data if item!='_id']
elif isinstance(file_data, dict):
ids = list(file_data.keys())
data = list(file_data.values())
else:
logging.warning(f"測試用例 {testcase_node_id} 資料格式錯誤, 應為列表或字典格式")
if ddt and isinstance(data, list):
metafunc.parametrize("case_data", data, ids=ids, scope="function")
else: # 不啟用ddt時,整體作為一個資料
metafunc.parametrize("case_data", [file_data], scope="function")
用例執行效果如下