【pytest 教學】#03 fixture 機制:測試的前置準備與資源管理

測驗:pytest fixture 機制

共 5 題,點選答案後會立即顯示結果

1. fixture 在 pytest 中的主要用途是什麼?

  • A. 用來執行測試並產生測試報告
  • B. 在測試開始前準備好需要的資源或資料
  • C. 用來標記哪些函式是測試函式
  • D. 用來設定測試的執行順序

2. 以下程式碼中,pytest 如何知道要把 user fixture 傳給測試函式?

@pytest.fixture def user(): return User(name=”Alice”) def test_user_name(user): assert user.name == “Alice”
  • A. 透過 @pytest.fixture 裝飾器自動綁定
  • B. 必須手動呼叫 user() 函式
  • C. 透過測試函式的參數名稱與 fixture 名稱匹配
  • D. 透過 import 語句導入 fixture

3. 關於 fixture 的 scope 設定,下列敘述何者正確?

  • A. scope="function" 表示整個測試檔案只建立一次
  • B. scope="session" 表示整個 pytest 執行只建立一次
  • C. scope="module" 表示每個測試類別建立一次
  • D. 預設的 scope 是 "session"

4. 在 yield fixture 中,yield 之後的程式碼何時執行?

@pytest.fixture def temp_file(): f = open(“test.txt”, “w”) yield f f.close() os.remove(“test.txt”)
  • A. 在測試函式執行之前
  • B. 與 yield 之前的程式碼同時執行
  • C. 只有在測試失敗時才會執行
  • D. 在測試函式執行完畢之後

5. conftest.py 檔案的作用是什麼?

  • A. 設定 pytest 的執行參數和外掛程式
  • B. 放置可跨多個測試檔案共用的 fixture
  • C. 定義測試的預期輸出結果
  • D. 記錄測試執行的歷史紀錄

一句話說明

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)

執行順序

  1. pytest 看到 test_user 需要 user
  2. 發現 user fixture 需要 db
  3. 先執行 db() 得到資料庫
  4. 再執行 user(db) 得到使用者
  5. 最後執行測試

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 題,包含情境題與錯誤診斷題。

1. 你正在寫一個電商系統的測試,需要測試訂單、購物車、結帳等多個模組。每個測試都需要資料庫連線,但建立連線很耗時。你應該如何設定 fixture? 情境題

  • A. 使用 @pytest.fixture 不加任何參數,讓每個測試都有獨立連線
  • B. 使用 @pytest.fixture(scope="session"),整個測試只建立一次連線
  • C. 使用 @pytest.fixture(scope="class"),每個測試類別建立一次連線
  • D. 不使用 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()
  • A. 應該使用 scope="module" 來確保檔案在模組結束時刪除
  • B. temp_file.seek(0) 的呼叫方式錯誤
  • C. 使用 return 後程式碼不會執行,應該改用 yield
  • D. 應該在測試函式中手動呼叫 f.close()

3. 你的專案有以下測試目錄結構,想讓 db fixture 只給 api 目錄下的測試使用。fixture 應該放在哪裡? 情境題

tests/ ├── conftest.py ├── api/ │ ├── conftest.py │ ├── test_users.py │ └── test_orders.py └── unit/ ├── conftest.py └── test_utils.py
  • A. 放在 tests/conftest.py
  • B. 放在 tests/api/conftest.py
  • C. 放在 tests/unit/conftest.py
  • D. 同時放在 test_users.pytest_orders.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”)
  • A. 應該把 yield 改成 return
  • B. 應該使用 @pytest.fixture(params=["mysql", "postgres", "sqlite"]) 並搭配 request.param
  • C. 應該把 fixture 放在 conftest.py
  • D. 應該在測試函式中使用 for 迴圈

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
  • A. 使用 @pytest.fixture(scope="session")
  • B. 把 fixture 放到 conftest.py 就會自動套用
  • C. 使用 @pytest.fixture(autouse=True)
  • D. 在每個測試檔案開頭 import 這個 fixture

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *