Python - PyTest Fixture
在 pytest 裡,fixture 是一種用來管理測試前置條件(setup)與資源生命週期(teardown)的機制。
也就是我們可以把重複的初始化邏輯抽出來、讓測試之間共享資源(ex. DB 連線、API client、測試資料),以及控制資源建立與釋放的時機(function / class / module / session scope),進而提升測試的可讀性與可維護性。
在實務上通常會用 fixture 建立這些咚咚:
- mock API client
- test database
- temporary file
- 測試用 user object
- 環境變數設定
@pytest.fixture
def db_connection():
conn = connect_to_db()
yield conn
conn.close()
在這邊 yield 前是 setup 而 yield 後是 teardown。
不需要寫 try/finally 因為 pytest 會幫忙管理 (不如說 pytest 其實就是把 yield 轉成類似 try/finally 的機制)。
如果 setup 在 yield 前發生例外,那 teardown 就不會執行。
另外,fixture 之間是可以互相依賴的,pytest 會自動解析 dependency graph。
@pytest.fixture
def user():
return {"id": 1}
@pytest.fixture
def auth_token(user):
return f"token_for_{user['id']}"
conftest.py
conftest.py 是 pytest 的共享 fixture 定義檔,會自動被 pytest 掃描,不需要 import 就能使用。
可以把 fixture 想成一種測試基礎設施(test infrastructure layer),讓測試只專注在 assertion,而不是 setup 邏輯。
conftest.py 作用範圍是 該目錄及其子目錄;pytest 執行時會往 當前測試檔案的目錄往上 搜尋 conftest.py。
簡單一點講的話,通常 tests/ 底下會有一個,但 test/ 裡面的巢狀資料夾也可以有多個 conftest.py。
scope (生命週期)
使用 scope 可以控制 fixture 什麼時候建立、多久建立一次。
透過這個架構設計就可以控制測試速度、資源管理、測試隔離性。
function (default)
每個 test function 都會建立一次、最安全(test isolation 最好),最不會有狀態污染問題。
# conftest.py
@pytest.fixture(scope="function")
def user():
print("create user")
return {"name": "Jessie"}
# test/test_script.py
class TestUser:
def test_a(user): ...
def test_b(user): ...
測試輸出:
create user
create user
class
class scope fixture 只會對 使用它的 class 生效。 實務上就是每個 test class 建一次,也就是同一個 class 裡共用。
# conftest.py
@pytest.fixture(scope="class")
def db():
print("connect db")
return connect()
# test/test_script.py
class TestUser:
def test_a(self, db): ...
def test_b(self, db): ...
測試輸出:
connect db
不同 class 會重新建。
module
每個檔案只建一次,簡單說就是整個 module (.py 測試檔) 共用。
# conftest.py
@pytest.fixture(scope="module")
def api_client():
print("create client")
return Client()
# test/test_script.py
class TestUserInContryA:
def test_a1(self, api_client): ...
def test_a2(self, api_client): ...
class TestUserInContryB:
def test_b1(self, api_client): ...
def test_b2(self, api_client): ...
測試輸出:
create client
session
整個 pytest run 只建一次,跨檔案共享。
# conftest.py
@pytest.fixture(scope="session")
def test_env():
print("setup environment")
return setup_env()
測試輸出:
setup environment
在 單執行緒 情況下,不管 test/ 有成千上百個測試都要跑都只建一次。
autouse
設定 autouse=True 之後,每個 test 都會自動執行(不需要在 test 裡寫參數),適合做全域初始化。
但要謹慎使用 autouse,避免測試隱性依賴,降低可讀性。
# conftest.py
@pytest.fixture(autouse=True)
def set_env():
os.environ["ENV"] = "test"
Advanced Fixture Patterns
State Pollution (狀態污染)
當 fixture 包含 mutable object 時,需要注意 State Pollution 問題,避免 state leakage。
# conftest.py
@pytest.fixture(scope="module")
def data():
return []
# test/test_script.py
def test_a(data):
data.append(1)
def test_b(data):
assert data == [] # ❌ 會 fail up 因為這時候 data 已經有了 [1]
因為 module scope 共享同一個 list。
(詳見另一篇 Python - Syntax & Runtime 的 Mutable Default Arguments 章節)
Expensive Resource 設計策略
實務上要如何選擇合適的 scope 平衡測試隔離性及執行成本?
如果 API client 建立成本低,可以用 function scope 確保每次測試互相獨立。
但如果需要啟動 Docker container 或連接真實 DB 等成本較高的行為,選用 module 或 session scope 會得到比較理想的執行時間。
Teardown 發生時機
@pytest.fixture(scope="module")
def db():
conn = connect()
yield conn
conn.close()
teardown 會在 scope 結束時 才執行
module → 整個檔案結束才 close
session → 整個 pytest run 結束才 close
Parallel 測試
如果用 pytest-xdist 平行測試,每個 worker 會有自己的 session scope 而不是大家共用。
此時 session ≠ 全域唯一。