如果你還想從頭學起Pytest,可以看看這個系列的文章哦!
https://www.cnblogs.com/poloyy/category/1690628.html
背景
- 使用 pytest-xdist 分散式外掛可以加快執行,充分利用機器多核 CPU 的優勢
- 將常用功能放到 fixture,可以提高複用性和維護性
- 做介面自動化測試的時候,通常我們會將登入介面放到 fixture 裡面,並且 scope 會設定為 session,讓他全域性只執行一次
- 但是當使用 pytest-xdist 的時候,scope=session 的 fixture 無法保證只執行一次,官方也通報了這一問題
官方描述
- pytest-xdist 的設計使每個工作程式將執行自己的測試集合並執行所有測試子集,這意味著在不同的測試過程中,要求高階範圍的 fixture(如:session)將會被多次執行,這超出了預期,在某些情況下可能是不希望的
- 儘管 pytest-xdist 沒有內建支援來確保 scope=session 的fixture 僅執行一次,但是可以通過使用鎖定檔案進行程式間通訊來實現
前置知識
pytest-xdist 分散式外掛使用詳細教程可看
https://www.cnblogs.com/poloyy/p/12694861.html
分散式外掛原理可看
https://www.cnblogs.com/poloyy/p/12703290.html
fixture 的使用詳細教程
https://www.cnblogs.com/poloyy/p/12642602.html
官方文件
https://pypi.org/project/pytest-xdist/
官方解決辦法(直接套用就行)
import json import pytest from filelock import FileLock @pytest.fixture(scope="session") def session_data(tmp_path_factory, worker_id): if worker_id == "master": # not executing in with multiple workers, just produce the data and let # pytest's fixture caching do its job return produce_expensive_data() # get the temp directory shared by all workers root_tmp_dir = tmp_path_factory.getbasetemp().parent fn = root_tmp_dir / "data.json" with FileLock(str(fn) + ".lock"): if fn.is_file(): data = json.loads(fn.read_text()) else: data = produce_expensive_data() fn.write_text(json.dumps(data)) return data
- 若某個 scope = session 的 fixture 需要確保只執行一次的話,可以用上面的方法,直接套用,然後改需要改的部分即可(這個後面詳細講解)
- 官方原話:這項技術可能並非在每種情況下都適用,但對於許多情況下,它應該是一個起點,在這種情況下,對於 scope = session 的fixture 只執行一次很重要
後續栗子的程式碼
專案結構
xdist+fixture(資料夾) │ tmp(存放 allure 資料資料夾) │ conftest.py │ test_1.py │ test_2.py │ test_3.py │ __init__.py │
test_1.py 程式碼
import os def test_1(test): print("os 環境變數",os.environ['token']) print("test1 測試用例", test)
test_2.py 程式碼
import os def test_2(test): print("os 環境變數",os.environ['token']) print("test2 測試用例", test)
test_3.py 程式碼
import os def test_3(test): print("os 環境變數",os.environ['token']) print("test3 測試用例", test)
未解決情況下的栗子
conftest.py 程式碼
import os import pytest from random import random @pytest.fixture(scope="session") def test(): token = str(random()) print("fixture:請求登入介面,獲取token", token) os.environ['token'] = token return token
執行命令
pytest -n 3 --alluredir=tmp
執行結果
scope=session 的 fixture 很明顯執行了三次,三個程式下的三個測試用例得到的資料不一樣,明顯不會是我們想要的結果
使用官方解決方法的栗子rt
#!/usr/bin/env python # -*- coding: utf-8 -*- """ __title__ = __Time__ = 2021/4/27 11:28 __Author__ = 小菠蘿測試筆記 __Blog__ = https://www.cnblogs.com/poloyy/ """ import json import os import pytest from random import random from filelock import FileLock @pytest.fixture(scope="session") def test(tmp_path_factory, worker_id): # 如果是單機執行 則執行這裡的程式碼塊【不可刪除、修改】 if worker_id == "master": """ 【自定義程式碼塊】 這裡就寫你要本身應該要做的操作,比如:登入請求、新增資料、清空資料庫歷史資料等等 """ token = str(random()) print("fixture:請求登入介面,獲取token", token) os.environ['token'] = token # 如果測試用例有需要,可以返回對應的資料,比如 token return token # 如果是分散式執行 # 獲取所有子節點共享的臨時目錄,無需修改【不可刪除、修改】 root_tmp_dir = tmp_path_factory.getbasetemp().parent # 【不可刪除、修改】 fn = root_tmp_dir / "data.json" # 【不可刪除、修改】 with FileLock(str(fn) + ".lock"): # 【不可刪除、修改】 if fn.is_file(): # 快取檔案中讀取資料,像登入操作的話就是 token 【不可刪除、修改】 token = json.loads(fn.read_text()) print(f"讀取快取檔案,token 是{token} ") else: """ 【自定義程式碼塊】 跟上面 if 的程式碼塊一樣就行 """ token = str(random()) print("fixture:請求登入介面,獲取token", token) # 【不可刪除、修改】 fn.write_text(json.dumps(token)) print(f"首次執行,token 是{token} ") # 最好將後續需要保留的資料存在某個地方,比如這裡是 os 的環境變數 os.environ['token'] = token return token
執行命令
pytest -n 3 --alluredir=tmp
執行結果
可以看到 fixture 只執行了一次,不同程式下的測試用例共享一個資料 token
重點
- 讀取快取檔案並不是每個測試用例都會讀,它是按照程式來讀取的
- 比如 -n 3 指定三個程式執行,那麼有一個程式會執行一次 fixture(隨機),另外兩個程式會各讀一次快取
- 假設每個程式有很多個用例,那也只是讀一次快取檔案,而不會讀多次快取檔案
- 所以最好要將從快取檔案讀出來的資料儲存在特定的地方,比如上面程式碼的 os.environ 可以將資料儲存在環境變數中
兩個程式跑三個測試用例檔案
還是上面栗子的程式碼
執行命令
pytest -n 2 --alluredir=tmp
執行結果
可以看到 test_3 的測試用例就沒有讀快取檔案了,每個程式只會讀一次快取檔案,記住哦!