跳至主要内容

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 ≠ 全域唯一。