在測試的時候,很多時候都需要一些前置 / 後置作業 (如建立測試資料,測試後需要刪除等) 需要處理。而這些作業,會希望被不同的 Test Case 共用。在 PyTest 中需要做到這些效果,要用到 Conftest.py
和 fixture
的功能,因此我們需要學習使用以減少一些重覆性的程式。
Conftest.py
的用法WebDriver / Requests Session 初始化
下例是把 Chrome WebDriver 的設定寫成 Fixture,作為各 UI Test Cases 的前置動作。
@pytest.fixture()
def driver():
# Chrome Driver 的設定
options = webdriver.ChromeOptions()
# 啟用 Headless 的模式,讓測試執行時不會顯示執行畫面,節省資源
options.add_argument('--headless')
# Headless 需要指定 Window Size,否則會因為執行的畫面太少導致非預期的錯誤
options.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=options)
# 根據本機的螢幕大小 / Window Size 的設定判斷,放到最大
driver.maximize_window()
driver.get(index_url)
return driver
# 把 fixture 作為參數傳入,會得到 fixture return 的結果作為參數
# 執行 test_one 的時候,會先執行 driver()
def test_one(driver):
pass
# 若 test case 不需要使用 fixture 這參數
@pytest.mark.usefixtures("driver")
def test_two()
pass
連接初始化 (I.e. DataBase 的連接)
@pytest.fixture(scope=session)
def db_connection():
db_settings = {
"user": "xxxxx",
"password": "xxxxx",
"host": "xxx.xxx.xxx.xxx",
"database": "xxxxx",
"port" = 3306,
"cursorclass": DictCursor,
"read_timeout" = 5,
}
conn = pymysql.connect(**db_settings)
return conn
def test_with_db(db_connection)
pass
數據初始化 (I.e. Test Data 的準備)
主要作用是在測試過程中 不管是順利完成還是突然中斷 都會回來執行 配置銷毀 /資料還原 的動作。
例如上 2 個程式的例子,Chrome Driver 被測試程式應用以後,需要進行關閉。或是 Database 在使用後會關掉連線。會有以下 3 個做法:
以 yield
取代 return
@pytest.fixture()
def driver():
options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=options)
driver.maximize_window()
driver.get(index_url)
# return 改成 yield,讓執行測試程式後會回來這裡執行後面的動作
yield driver
# 測試結束後,回關閉 driver
driver.quit()
使用 with
的寫法:
@pytest.fixture(scope=session)
def db_connection():
with pymysql.connect(**db_settings) as connection:
print("Start connection...")
yield connection
print("End connection...")
使用 addfinalizer
的寫法:
Fixture 有一個特殊的參數 request
,讓您可以存取有關當前測試執行的上下文和相關資訊。這裡會應用 request 的 addfinalizer。
@pytest.fixture()
def create_order(request):
print("create user")
print("create order")
# finalizer 的 function
def delete_order():
print("delete order")
def delete_user():
print("delete user")
# 註冊為終結 function,會在測試程式終結後回來執行 finalizer 的程式。
# 注意執行的順序會跟註冊的順序相反,所以這裡會先執行 delete_order,再執行 delete_user
request.addfinalizer(delete_user)
request.addfinalizer(delete_order)
def test_order_check(create_order):
print("Check order and user info")
addfinalizer
跟 yield
最大的分別,是在執行 yield
後面程式碼的過程中,若發生錯誤就無法往下繼續執行。而 addfinalizer
在其中一個 finalizer
執行出現錯誤,還會執行其他的 finalizer
。
測試資料還原的重要性
為了測試的穩定性,我們常會建立新的測試資料來避免髒資料影響測試結果,所以同時在測試結束後,需要把資料刪除。也防止不該存在資料影響其他的測試,以確保有乾淨、穩定測試環境。
有人會誤把還原資料的步驗寫在 Test Case 的最後,但這並不是好的做法,因為每次測試你都無法確保他能走到最後一步,中途測試失敗就會跳出了。為確保會執行資料還原的程式,務必使用上述的方法。
fixture
可以設定一個叫 scope
的參數,用來實現 fixture
在不同層級的數據共享。
在指定層級內,將會只執行一次 fixture
的內容。
共有 5 個 層級 (Scope):
scope
的預設值。.py
) 內不管有多少 class 或 function 都只執行一次 Fixture。以 Chrome Driver 的初始建立為例,Chrome Driver 都必須在每個 Test Case 重啟,避免受上一個 Test Case 影響會,因此會使用到 scope=function
,上例沒有特別設定則是應用了預設值。
而 Database 連接的初始化,在整個測試的流程,只需連接一次即可,在不同的 Test Case 就可以應用 SQL 去取得不同的資料,直到所有 Test Case 完成才關閉連線,那設定 scope=session
。
再給一個例子,類似的 Test Case 可能需要從 Database 得到相同的資料作測試,那從 DB 取資料只需要一次便足夠,以減少資源的浪費。那需要根據 Test Cases 分佈的層級選擇所需的 Scope。
是一個在 PyTest 的特殊文件,這個文件的名稱是固定的,它可以存在於測試目錄甚至根目錄,將影響該目錄及其子目錄中的測試模塊。執行測試前會先執行 Conftest.py
,用於共享配置、 fixtures
和插件等内容。
為了讓 Fixture 可以跨 .py
調用,可以將共用的 fixture 放在 conftest.py
中,在同一個資料夾下的所有測試文件都可以使用。名字是固定的,PyTest 會自動識別該文件。
包在同一個資料夾下,只有該資料夾下的測試文件可使用:
├── tests_web
├── conftest.py 含 driver fixture
└── test_a.py 可以使用 driver fixture
└── tests_api
└── test_b.py 不可以使用 driver fixture
子層的資料夾下的測試文件當亦可使用
├── conftest.py 含 db connection fixture
├── tests_web
└── test_a.py 可以使用 db connection fixture
└── tests_api
└── test_b.py 可以使用 db connection fixture
在同 Package 下,conftest.py
的 fixture 可以測試檔案,甚至是子層的 fixture 覆寫,執行會套用覆寫的 Fixture
├── tests_web
├── conftest.py 含 driver fixture
├── test_a.py 覆寫 driver fixture, 執行 test_a.py 時會應用此 driver fixture
└── test_b.py 應用 conftest.py 的 driver fixture
以 初始 WebDriver Fixture 為例,一般會寫到 conftest.py 被所有 UI 測試文件共用。
而 連接 Database 的 Fixture 為例,會移到最外層的 conftest.py 被所以測試文件共用。
此外,conftest.py
還有一個蠻常的應用是建立執行 PyTest command 的選項。
如下例增加一個選項以決定用哪種 Browser 來進行測試。
# conftest.py
import pytest
from selenium.webdriver.chrome.service import Service as chromeService
from selenium.webdriver.firefox.service import Service as firefoxService
from webdriver_manager.firefox import GeckoDriverManager
from selenium import webdriver
# 新增 PyTest command 選項的 function
def pytest_addoption(parser):
# --browser 是把參數名為 browser
# action="store" 表示將這選項的值 儲存 在變數中
# default="chrome" 表示若 user 沒有輸入這選項時,default 為 "chrome"
# help="xxx" 則是當 user 輸入 pytest --help 時,系統會顯示的提示
parser.addoption("--browser", action="store",
default="chrome", help="Input Testing Browser: chrome or firefox")
@pytest.fixture
# 在需要應用該選項資料的 function 帶入 pytest 內建的 request fixture
def driver(request):
# 應用 request.config.getoption 取得選項的值
browser = request.config.getoption("--browser")
driver = None
# 若為 chrome 時,建 chrome 的 webdriver
if browser == "chrome":
driver = webdriver.Chrome(service=chromeService())
# 若為 firefox 時,建 firefox 的 webdriver
elif browser == "firefox":
driver = webdriver.Firefox(service=firefoxService(GeckoDriverManager().install()))
yield driver
driver.quit()
配合應用 Conftest.py
和 fixture
,現在你的專案架構已經變成這樣了:
├── tests_web
├── conftest.py # 可放 driver fixture
└── test_web_*.py
├── tests_api
└── test_api_*.py
├── conftest.py # 可放 DB connection fixture
├── pytest.ini
└── requirement.txt
再套用之前已講述的 Page Object 和 API Object 合併進來,就會變成:
├── api_objects
├── __init__.py
└── *_api.py
├── page_objects
├── __init__.py
├── *_page.py
└── action_utils.py
├── tests_web
├── conftest.py # 含 driver fixture
└── test_web_*.py
├── tests_api
└── test_api_*.py
├── conftest.py # 含 DB connection fixture
├── pytest.ini
└── requirement.txt
框架開始愈來愈大了,已經可以開始寫 Test Case 囉。當然這個架構只供參考,因應不同的專案需求,會有不一樣的設計。現在可以先跟著這個把練習用的測試網站寫成 Test Case。