測驗:pytest fixture 機制
共 5 題,點選答案後會立即顯示結果
1. fixture 在 pytest 中的主要用途是什麼?
2. 以下程式碼中,pytest 如何知道要把 user fixture 傳給測試函式?
@pytest.fixture
def user():
return User(name=”Alice”)
def test_user_name(user):
assert user.name == “Alice”
3. 關於 fixture 的 scope 設定,下列敘述何者正確?
4. 在 yield fixture 中,yield 之後的程式碼何時執行?
@pytest.fixture
def temp_file():
f = open(“test.txt”, “w”)
yield f
f.close()
os.remove(“test.txt”)
5. conftest.py 檔案的作用是什麼?
一句話說明
fixture 是「測試開始前幫你準備好東西」的機制。
為什麼需要 fixture?
寫測試時,常常需要先準備一些東西:
# 沒有 fixture 的寫法:每個測試都要重複準備
def test_user_name():
user = User(name="Alice", age=25) # 重複!
assert user.name == "Alice"
def test_user_age():
user = User(name="Alice", age=25) # 又重複!
assert user.age == 25
def test_user_is_adult():
user = User(name="Alice", age=25) # 還是重複!
assert user.is_adult() == True
Code language: PHP (php)問題:同樣的準備程式碼寫了三次。如果要改,就要改三個地方。
最小範例
import pytest
@pytest.fixture
def user():
return User(name="Alice", age=25)
def test_user_name(user): # user 自動注入
assert user.name == "Alice"
def test_user_age(user): # user 自動注入
assert user.age == 25
Code language: PHP (php)翻譯:
@pytest.fixture把函式變成「fixture」- 測試函式的參數名稱
user對應到 fixture 名稱 - pytest 會自動幫你呼叫 fixture,把結果傳進來
逐行翻譯
@pytest.fixture # 告訴 pytest:這是一個 fixture
def user(): # fixture 的名稱是 "user"
return User(...) # 回傳準備好的東西
def test_xxx(user): # 參數名稱 = fixture 名稱
... # pytest 自動把 user fixture 的回傳值傳進來
Code language: PHP (php)核心概念:參數名稱決定注入哪個 fixture。
fixture 的依賴注入
pytest 會看你的測試函式「要什麼」,自動給你。
@pytest.fixture
def db():
return Database()
@pytest.fixture
def user(db): # fixture 也可以依賴其他 fixture!
return db.create_user(name="Alice")
def test_user(user): # 只要 user,pytest 會自動先建立 db
assert user.name == "Alice"
Code language: PHP (php)執行順序:
- pytest 看到
test_user需要user - 發現
userfixture 需要db - 先執行
db()得到資料庫 - 再執行
user(db)得到使用者 - 最後執行測試
fixture 的 scope:控制生命週期
問題:每個測試都重新建立,太慢了
@pytest.fixture
def db():
return connect_to_database() # 假設這很慢
def test_1(db): ... # 連線一次
def test_2(db): ... # 又連線一次
def test_3(db): ... # 再連線一次
Code language: PHP (php)解法:用 scope 控制「活多久」
@pytest.fixture(scope="module") # 整個檔案只建立一次
def db():
return connect_to_database()
def test_1(db): ... # 用同一個連線
def test_2(db): ... # 用同一個連線
def test_3(db): ... # 用同一個連線
Code language: PHP (php)scope 對照表
| scope | 翻譯 | 何時重新建立 |
|---|---|---|
"function" |
每個測試 | 每個 test_ 函式(預設) |
"class" |
每個測試類別 | 每個 class |
"module" |
每個檔案 | 每個 .py 檔 |
"session" |
整個測試 | 整個 pytest 執行只建立一次 |
選哪個?
# function(預設):測試間要完全隔離
@pytest.fixture # 不寫 scope,預設是 function
def temp_file():
return create_temp_file()
# module:同一檔案的測試共用,但建立成本高
@pytest.fixture(scope="module")
def db_connection():
return connect_to_db()
# session:整個測試共用,建立一次就好
@pytest.fixture(scope="session")
def browser():
return launch_browser()
Code language: PHP (php)yield fixture:有借有還
有些資源用完要清理(關檔案、斷連線、刪暫存)。
最小範例
@pytest.fixture
def temp_file():
# --- setup(測試前)---
f = open("test.txt", "w")
yield f # ← 把 f 交給測試用
# --- teardown(測試後)---
f.close()
os.remove("test.txt")
Code language: PHP (php)執行流程
1. 執行 yield 之前的程式碼(setup)
2. yield 把值交給測試
3. 測試執行
4. 測試結束後,執行 yield 之後的程式碼(teardown)
Code language: JavaScript (javascript)常見用法
# 資料庫交易:測完自動 rollback
@pytest.fixture
def db_session(db):
session = db.create_session()
yield session
session.rollback() # 測試資料不會真的寫入
# 暫存目錄:測完自動刪除
@pytest.fixture
def temp_dir():
path = Path("./temp_test")
path.mkdir()
yield path
shutil.rmtree(path)
# Mock 外部服務:測完自動還原
@pytest.fixture
def mock_api(monkeypatch):
monkeypatch.setattr(requests, "get", fake_get)
yield
# monkeypatch 會自動還原,不用寫 teardown
Code language: PHP (php)conftest.py:跨檔案共享 fixture
問題:每個測試檔都要寫一樣的 fixture
tests/
├── test_user.py # 需要 db fixture
├── test_order.py # 也需要 db fixture
└── test_product.py # 還是需要 db fixture
Code language: PHP (php)解法:放到 conftest.py
tests/
├── conftest.py # 把共用 fixture 放這裡
├── test_user.py
├── test_order.py
└── test_product.py
Code language: PHP (php)# tests/conftest.py
import pytest
@pytest.fixture(scope="session")
def db():
return connect_to_database()
@pytest.fixture
def user(db):
return db.create_user(name="Alice")
Code language: PHP (php)# tests/test_user.py
def test_user_name(user): # 自動找到 conftest.py 裡的 fixture
assert user.name == "Alice"
Code language: PHP (php)conftest.py 的特性
- 檔名必須是
conftest.py(不能改) - pytest 自動載入,不用 import
- 可以放在不同層級,就近優先
tests/
├── conftest.py # 所有測試都能用
├── api/
│ ├── conftest.py # 只有 api/ 下的測試能用
│ └── test_api.py
└── unit/
└── test_unit.py
Code language: PHP (php)常見變化
變化 1:fixture 回傳工廠函式
@pytest.fixture
def make_user():
def _make_user(name, age=25):
return User(name=name, age=age)
return _make_user
def test_users(make_user):
alice = make_user("Alice")
bob = make_user("Bob", age=30)
Code language: JavaScript (javascript)翻譯:當你需要建立「多個類似物件」時,回傳一個建立函式。
變化 2:參數化 fixture
@pytest.fixture(params=["mysql", "postgres", "sqlite"])
def db(request):
return connect_to(request.param)
def test_query(db): # 會跑 3 次,每次不同資料庫
assert db.query("SELECT 1")
Code language: PHP (php)翻譯:一個 fixture 產生多種變化,每種都測一遍。
變化 3:autouse 自動套用
@pytest.fixture(autouse=True)
def setup_logging():
logging.basicConfig(level=logging.DEBUG)
yield
logging.shutdown()
def test_something(): # 不用寫參數,自動套用
...
Code language: PHP (php)翻譯:autouse=True 讓 fixture 自動對所有測試生效。
AI 最常這樣用
模式 1:準備測試資料
@pytest.fixture
def sample_data():
return {
"users": [{"name": "Alice"}, {"name": "Bob"}],
"products": [{"id": 1, "name": "Phone"}]
}
Code language: CSS (css)模式 2:Mock 外部依賴
@pytest.fixture
def mock_api(mocker):
return mocker.patch("app.external_api.call")
Code language: JavaScript (javascript)模式 3:設定測試環境
@pytest.fixture(scope="session")
def app():
app = create_app(config="testing")
yield app
app.cleanup()
Code language: JavaScript (javascript)Vibe Coder 檢查點
看到 fixture 時確認:
- [ ] fixture 的名稱有描述性嗎?(看得出是什麼)
- [ ] scope 設定合理嗎?(需要隔離就用 function,需要共用就提高 scope)
- [ ] 用 yield 的 fixture 有清理資源嗎?(開了要關)
- [ ] 共用的 fixture 有放到 conftest.py 嗎?
- [ ] 有沒有過度使用 autouse?(可能造成意外副作用)
快速對照表
| 你會看到 | 意思 |
|---|---|
@pytest.fixture |
這是一個 fixture |
def test_xxx(some_fixture): |
測試需要 some_fixture |
scope="session" |
整個測試只建立一次 |
yield something |
setup/teardown 模式 |
conftest.py |
共用 fixture 放這裡 |
autouse=True |
自動套用,不用寫參數 |
params=[...] |
參數化,每個參數跑一次 |
總結
fixture 解決的問題:
├── 重複的準備程式碼 → 抽成 fixture
├── 資源的清理 → 用 yield
├── 執行效率 → 調整 scope
└── 跨檔案共用 → 放 conftest.py
Code language: JavaScript (javascript)下一篇,我們會學習如何使用參數化測試,讓一個測試涵蓋多種輸入情境。
進階測驗:pytest fixture 機制
測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。
共 5 題,包含情境題與錯誤診斷題。
1. 你正在寫一個電商系統的測試,需要測試訂單、購物車、結帳等多個模組。每個測試都需要資料庫連線,但建立連線很耗時。你應該如何設定 fixture? 情境題
2. 小明寫了以下測試程式碼,但發現測試後暫存檔案沒有被刪除。問題出在哪裡? 錯誤診斷
@pytest.fixture
def temp_file():
f = open(“test_data.txt”, “w”)
f.write(“test content”)
return f
f.close()
os.remove(“test_data.txt”)
def test_read_file(temp_file):
temp_file.seek(0)
assert “test” in temp_file.read()
3. 你的專案有以下測試目錄結構,想讓 db fixture 只給 api 目錄下的測試使用。fixture 應該放在哪裡? 情境題
tests/
├── conftest.py
├── api/
│ ├── conftest.py
│ ├── test_users.py
│ └── test_orders.py
└── unit/
├── conftest.py
└── test_utils.py
4. 小華想測試他的函式在不同資料庫(MySQL、PostgreSQL、SQLite)上都能正常運作。他寫了以下程式碼但測試只跑了一次。哪裡需要修正? 錯誤診斷
@pytest.fixture
def db():
databases = [“mysql”, “postgres”, “sqlite”]
for db_type in databases:
yield connect_to(db_type)
def test_query(db):
assert db.query(“SELECT 1”)
5. 你正在開發一個需要在每個測試前自動設定 logging 的專案。你不想在每個測試函式的參數中都加上這個 fixture。應該怎麼做? 情境題
@pytest.fixture
def setup_logging():
logging.basicConfig(level=logging.DEBUG)
yield
logging.shutdown()
# 希望不用寫參數就能自動套用
def test_something():
logging.debug(“test message”)
assert True