iT邦幫忙

2023 iThome 鐵人賽

DAY 26
0

學習原因:

在測試的時候,很多時候都需要一些前置 / 後置作業 (如建立測試資料,測試後需要刪除等) 需要處理。而這些作業,會希望被不同的 Test Case 共用。在 PyTest 中需要做到這些效果,要用到 Conftest.pyfixture 的功能,因此我們需要學習使用以減少一些重覆性的程式。

學習目標:

  • Fixture 在初始化與配置銷毀 / 資料還原的應用
  • 了解 Fixture 在不同的層級 (Scope) 使用的分別,以及其執行順序
  • 了解 Conftest.py 的用法

Fixture 在初始化 (Setup) 中常見的應用

  • 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 的準備)

Fixture 在配置銷毀 / 資料還原的應用

主要作用是在測試過程中 不管是順利完成還是突然中斷 都會回來執行 配置銷毀 /資料還原 的動作。

例如上 2 個程式的例子,Chrome Driver 被測試程式應用以後,需要進行關閉。或是 Database 在使用後會關掉連線。會有以下 3 個做法:

  1. 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()
    
  2. 使用 with 的寫法:

    @pytest.fixture(scope=session)
    def db_connection():
    	with pymysql.connect(**db_settings) as connection:
            print("Start connection...")
            yield connection
            print("End connection...")
    
  3. 使用 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")
    

    addfinalizeryield 最大的分別,是在執行 yield 後面程式碼的過程中,若發生錯誤就無法往下繼續執行。而 addfinalizer 在其中一個 finalizer 執行出現錯誤,還會執行其他的 finalizer

    測試資料還原的重要性

    為了測試的穩定性,我們常會建立新的測試資料來避免髒資料影響測試結果,所以同時在測試結束後,需要把資料刪除。也防止不該存在資料影響其他的測試,以確保有乾淨、穩定測試環境。

    有人會誤把還原資料的步驗寫在 Test Case 的最後,但這並不是好的做法,因為每次測試你都無法確保他能走到最後一步,中途測試失敗就會跳出了。為確保會執行資料還原的程式,務必使用上述的方法。

Fixture 可在不同的層級 (Scope) 使用

fixture 可以設定一個叫 scope 的參數,用來實現 fixture 在不同層級的數據共享。

在指定層級內,將會只執行一次 fixture 的內容。

共有 5 個 層級 (Scope)

  • Function: 指每個 Function 都會執行一次 fixture,為 scope 的預設值。
  • Class:以 Class 為單位,同一 Class 內只會執行一次 Fixture。
  • Module:以 Module 為單位,同一個 module (.py) 內不管有多少 class 或 function 都只執行一次 Fixture。
  • Package: 以 Package 為單位,同一個 Package 內 (資料夾) 內只執行一次 Fixture。
  • Session: 全局只會執行一次 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。

Conftest.py

是一個在 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.pyfixture,現在你的專案架構已經變成這樣了:

├── 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。


上一篇
Day 25: 應用 Jenkins 串接測試流程
下一篇
Day 27: Test Data 與 Environment Variable
系列文
從 0 開始培育成為自動化測試工程師的學習指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言