「好的專案結構就像整理得當的工具箱:開發者不需要看文件,也能在 3 秒內找到測試資料、Page Object 或工具函式放在哪裡。」
大家好,我是 Jane,一個每天跟 Python、pytest 和自動化測試架構打交道的自動化測試工程師(SDET)。
在過去幾天裡,我們從「單一腳本」聊到「測試框架核心(Day 6)」、「測試案例粒度(Day 7)」、「 Clean Code(Day 8)」以及「Page/Component Object 實戰(Day 9)」。
有了這些技術塊之後,下一個最常讓 QA 頭痛的問題就是:
conftest.py 該寫什麼?測試資料要放在哪?pages 資料夾該怎麼層級化?」
如果沒有規劃好專案結構,隨著測試案例累積到 50 個以上,你的專案很快就會出現 .py 檔案亂放、import 路徑超長且容易循環引用的災難。
今天這篇文章,我們就來手把手建立一個可擴充、結構清晰、符合 Python/pytest 生態系的最佳實踐(Best Practices)專案結構!
一個能維護三年的 pytest 自動化測試專案,通常具備以下 8 大層級:
┌────────────────────────────────────────────────────────────────┐
│ 1. Tests Layer (測試案例層) - tests/ │
│ - 僅包含 test_*.py,專注於語意化步驟與 pytest assert 斷言 │
├────────────────────────────────────────────────────────────────┤
│ 2. Business Flow / Service Layer (業務流程層 - 選填) │
│ - 組合多個 Page/Component 操作的複雜商業情境 │
├────────────────────────────────────────────────────────────────┤
│ 3. Page & Component Object Layer (頁面與元件物件層) - pages/ │
│ - 封裝 DOM Locator 與介面操作 (Click, Fill, Get) │
├────────────────────────────────────────────────────────────────┤
│ 4. Test Data Layer (測試資料層) - test_data/ fixtures/ │
│ - JSON/YAML 靜態資料或 Factory 產生的動態資料 │
├────────────────────────────────────────────────────────────────┤
│ 5. Fixtures & Lifecycle Layer (生命週期管理) - conftest.py │
│ - pytest 的 Fixture、Driver 初始化、環境變數載入 │
├────────────────────────────────────────────────────────────────┤
│ 6. Configuration Layer (設定檔) - config.py / .env / pytest.ini │
│ - 不同環境的 Base URL、Timeout、帳號密碼等配置 │
├────────────────────────────────────────────────────────────────┤
│ 7. Utilities & Helpers Layer (工具層) - utils/ │
│ - DB 連線、API Client、DB 清理、特製 Log/Report 工具 │
└────────────────────────────────────────────────────────────────┘
以下是推薦的標準 Python/pytest 自動化測試專案結構:
Plaintext
my_autotest_framework/
│
├── .env.example # 環境變數範本 (不可 commit 敏感資訊)
├── pytest.ini # pytest 核心設定 (如: addopts, markers, testpaths)
├── requirements.txt # 套件依賴需求 (playwright, pytest, python-dotenv 等)
│
├── config/ # 環境組態管理
│ ├── __init__.py
│ └── settings.py # 讀取 .env 的動態設定類別
│
├── pages/ # Page & Component Object 層
│ ├── __init__.py
│ ├── base_page.py # 所有 Page 的基底類別 (提供通用等待與操作)
│ ├── components/ # 可複用的 Component Objects
│ │ ├── __init__.py
│ │ ├── navbar_component.py
│ │ └── table_component.py
│ └── dashboard_page.py # 具體頁面 Object
│
├── test_data/ # 靜態測試資料庫
│ └── user_data.json
│
├── utils/ # 通用工具庫
│ ├── __init__.py
│ ├── api_client.py # 快速透過 API 建資料的 Helper
│ └── db_helper.py # 資料庫查詢與清理工具
│
├── tests/ # 測試案例層 (pytest 執行的核心)
│ ├── conftest.py # 全域/區域 pytest Fixtures (核心靈魂!)
│ ├── auth/ # 依模組/功能分資料夾
│ │ └── test_login.py
│ └── dashboard/
│ └── test_user_search.py
我們來看這個架構中的幾個關鍵檔案該怎麼寫:
config/settings.py)利用 python-dotenv 或 pydantic-settings 統一管理環境組態:
Python
# config/settings.py
import os
from dotenv import load_dotenv
# 載入 .env 檔案
load_dotenv()
class Settings:
BASE_URL: str = os.getenv("BASE_URL", "https://staging.example.com")
TIMEOUT: int = int(os.getenv("TIMEOUT", "10000")) # 毫秒
ADMIN_USER: str = os.getenv("ADMIN_USER", "admin_test")
ADMIN_PASS: str = os.getenv("ADMIN_PASS", "secure_password")
settings = Settings()
pages/base_page.py)提供所有 Page 物件共用的基礎能力:
Python
# pages/base_page.py
from playwright.sync_api import Page
from config.settings import settings
class BasePage:
def __init__(self, page: Page):
self.page = page
def navigate_to(self, path: str = ""):
full_url = f"{settings.BASE_URL}{path}"
self.page.goto(full_url)
def get_title(self) -> str:
return self.page.title()
tests/conftest.py)conftest.py 是 pytest 的大腦,負責提供 Page 物件、初始化頁面、處理失敗截圖與 Report Hooks:
Python
# tests/conftest.py
import pytest
from playwright.sync_api import Page
from pages.dashboard_page import DashboardPage
from config.settings import settings
@pytest.fixture(scope="function")
def dashboard_page(page: Page) -> DashboardPage:
"""自動注入 Playwright page 並初始化 DashboardPage 的 Fixture"""
dashboard = DashboardPage(page)
dashboard.navigate_to("/dashboard")
return dashboard
# 當測試失敗時自動截圖附加到 pytest 報告中
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
page: Page = item.funcargs.get("page")
if page:
# 截圖並儲存
page.screenshot(path=f"reports/screenshots/failure_{item.name}.png")
tests/dashboard/test_user_search.py)乾淨、極簡、語意明確的 pytest 測試:
Python
# tests/dashboard/test_user_search.py
from playwright.sync_api import expect
from pages.dashboard_page import DashboardPage
def test_admin_should_see_results_when_searching_valid_user(dashboard_page: DashboardPage):
"""
測試意圖: 管理員在 Dashboard 搜尋合法使用者時,應該列出該使用者資料
"""
# Act: 透過元件進行搜尋操作
dashboard_page.user_table.search("Jane")
# Assert: 斷言留在 Test Case 層,語意極度清晰
rows = dashboard_page.user_table.get_rows()
expect(rows).to_have_count(1)
tests/ 資料夾內部禁止出現硬編碼(No Hardcoding)
測試案例裡不可以出現 https://... 網址、帳號密碼或 .btn-primary 等定位器。網址走 config,定位器走 pages,資料走 test_data。
善用 pytest 的 conftest.py 作用域(Scope)
scope="session"。scope="function"(預設)。保持依賴單向流動,防止 Circular Import(循環引用)
依賴方向永遠應該是:
Tests > Pages / Business Flow > Components / BasePage > Config / Utils
千萬不要在 pages/ 裡面去引用 tests/ 裡面的 Fixture 或函式!
恭喜你!到今天 Day 10 為止,我們正式完成了一個具備良好架構、高可讀性且符合 Clean Code 原則的測試框架基礎。
我們從第二部分的 Day 6 到 Day 10,依序解鎖了:
從明天開始,我們將邁入整個系列文章最精彩、也最接地氣的《第三部分:處理最容易讓測試變脆弱的問題》。