引言
前面已經和大家介紹過 Unittest 測試框架的數據驅動框架 DDT,以及其實現原理。今天和大家分享的是 Pytest 測試框架的數據驅動,Pytest 測試框架的數據驅動是由 pytest 自帶的pytest.mark.parametrize()來實現的。
pytest.mark.parametrize 實現數據驅動
pytest.mark.parametrize 是 pytest 的內置裝飾器,它允許你在 function 或者 class 上定義多組參數和 fixture 來實現數據驅動。
@pytest.mark.parametrize() 裝飾器接收兩個參數:
第一個參數以字符串的形式存在,它代表能被被測試函數所能接受的參數,如果被測試函數有多個參數,則以逗號分隔;
第二個參數用於保存測試數據。如果只有一組數據,以列表的形式存在,如果有多組數據,以列表嵌套元組的形式存在(例如:[1,1]或者[(1,1), (2,2)])。
針對裝飾器的單參數和多參數,分別舉例如下。
1.pytest.mark.parametrize 單參數
# test_singal.py import pytest @pytest.mark.parametrize("number", [1, 0]) def test_equal(number): assert number == 1 if __name__ == "__main__": pytest.main([])
以上是單參數的一個例子,在這個例子中,test_equal 函數接收一個參數 number,這個參數有兩組數據,分別是 1 和 0。
tips:
裝飾器 pytest.mark.parametrize 的第一個參數里的參數名稱必須與測試函數中的參數稱保持一致。
即:test_equal這個函數方法的參數 number 必須與裝飾器里的第一個參數的名稱 number 保持一致。
運行以上代碼,結果如下圖所示:
可以看到,函數 test_equal 提供了兩組參數 1 和 0,所以它也執行了 2 次。
2.pytest.mark.parametrize 多參數
pytest.mark.parametrize 不僅支持單個參數,也可以支持多個參數,多個參數比較常見,因為在日常工作中,我們提供測試數據,不僅僅包括用於測試的數據,還包括用於驗證的數據,所以多參數還是比較常見的。
pytest.mark.parametrize 可以支持多參數,舉例如下:
# test_baidu.py import time import pytest from selenium import webdriver @pytest.mark.baidu class TestBaidu: def setup_method(self): self.driver = webdriver.Chrome() self.driver.implicitly_wait(30) self.base_url = "http://www.baidu.com/" @pytest.mark.parametrize('search_string, expect_string', [('Testing', 'Testing'), ('helloworld.com', 'Testing')]) def test_baidu_search(self, search_string, expect_string): driver = self.driver driver.get(self.base_url + "/") driver.find_element_by_id("kw").send_keys(search_string) driver.find_element_by_id("su").click() time.sleep(2) search_results = driver.find_element_by_xpath('//*[@id="1"]/h3/a').get_attribute('innerHTML') assert (expect_string in search_results) is True def teardown_method(self): self.driver.quit() if __name__ == "__main__": pytest.main(["-m", "baidu", "-s", "-v", "-k", "test_baidu_search", "test_baidu.py"])
上面這段代碼,被測試函數 test_baidu_search 有兩個參數,分別是 search_string 和 expect_string。那么對應着,在 pytest.mark.parametrize 這個裝飾器的第一個參數里,也包含 search_string 和 expect_string。
在命令行中運行結果如下:
pytest.fixture 擴展數據驅動
做過自動化測試的小伙伴,應該都很清楚地知道,無論 API 還是 UI 的自動化測試可以總結為三個步驟:
測試前的准備 —> 執行測試 —> 測試后的清理。
在日常的測試中,測試前的准備通常就是測試需要的前置條件,它可以是簡單的登錄操作、聯合查詢數據庫操作、測試數據讀取准備操作,甚至是邏輯復雜的函數操作。
和 unittest 框架一樣,在 pytest 中也可以通過使用 setup 和 teardown 來完成測試前置工作。
例如:
-
使用 setup_method、setup_class、setup_module 來分別完成測試類方法、測試類,以及測試 module 的 准備操作;
-
使用 teardown_method、teardown_class、teardown_module 來分別完成測試類方法、測試類,以及測試 module 清理操作。
但是這種方式存在一個比較明顯的缺陷。
例如,在同一個測試類中,存在多個測試方法,假設每一個測試方法需要不同的 setup 或者 teardown 函數,此時該怎么辦呢?
又比如,setup 和 teardown 其實都屬於測試夾具(Test Fixtures),如果我想把所有測試夾具全部放到一個函數中去管理,能做到嗎?
pytest 考慮到了這種情況,並且提供了一個更加高級的功能,那就是 fixture 裝飾器。
fixtures 可用作初始化測試服務、數據和狀態,也常常用來在測試執行前或測試執行后進行測試的前置操作或后置操作。
fixtures 可作為共享數據使用,也可被其他函數、模塊、類或者整個項目,甚至另外的 fixtures 調用。
1.fixtures 語法
pytest.fixtures 的語法如下:
fixture(scope="function", params=None, autouse=False, ids=None, name=None)
從語法可以看到 fixture 的5個參數如下:
scope:用於控制 fixture 的作用范圍
這個參數有以下4個級別:
-
function:在每一個 function 或者類方法中都會調用(默認)。
-
class:在每一個類中只調用一次。
-
module:每一個 .py 文件調用一次;該文件內可以有多個 function 和 class。
-
session:一個 session 調用一次。
params:一個可選的參數列表
params 以可選的參數列表形式存在。在測試函數中使用時,可通過 request.param 接收設置的返回值(即 params 列表里的值)。params 中有多少元素,在測試時,引用此 fixture 的函數就會調用幾次。
autouse:是否自動執行設置的 fixtures
當 autouse 為 True 時,測試函數即使不調用 fixture 裝飾器,定義的 fixture 函數也會被執行。
ids:指定每個字符串 id
當有多個 params 時,針對每一個 param,可以指定 id,這個 id 將變為測試用例名字的一部分。如果沒有提供 id,則 id 將自動生成。
name:fixture 的名稱
name 是 fixtures 的名稱, 它默認是你裝飾的那個 fixture 函數的名稱。你可以通過 name 參數來更改這個 fixture 名稱,更改后,如果這個 fixture 被調用,則使用你更改過的名稱即可。
2.fixtures 用法
fixtures 有多種使用方式,舉例說明如下。
(1)、通過 fixture 函數名直接使用
#test_fixture_usage.py import pytest # 首先, 在fixture函數上,加@pytest.fixture() @pytest.fixture() def my_method(): print('This is testing fixture') # 其次,把fixture函數的函數名作為參數,傳入被測試用例 def test_use_fixtures(my_method): print('Please follow Testing from WL')
通過 fixture 函數名使用 fixture 的步驟是:
-
在 fixture 函數上,加 @pytest.fixture(),上例中 my_method 這個方法將作為 fixture 使用;
-
把 fixture 函數的函數名作為參數,傳入被測試用例。
注意:函數 test_use_fixtures 的入參必須是 my_method 這個方法名,跟 fixture 函數保持一致。
通過運行以上代碼,在運行結果里,你會發現,my_method 即定義的 fixture 的方法先於測試函數的其他語句開始執行(相當於setup功能)。
(2)、通過 usefixtures 裝飾器使用
通過把 fixture 作為測試函數入參的方式,可以達到為每一個測試函數配置不同的 setup和teardown 的功能,但這樣會讓 fixture 和我的測試函數耦合在一塊,不利於測試函數的重用與測試框架的架構清晰。
因此 pytest 提供了 pytest.mark.usefixtures 這個裝飾器。
以下代碼舉例說明了 usefixtures 的具體用法:
#test_fixture_usage.py import pytest @pytest.fixture() def my_method(): print('This is Testing fixture') # 函數直接使用fixture @pytest.mark.usefixtures('my_method') def test_use_fixtures(): print('Please follow Testing from WL') class TestClass1: # 類方法使用fixture @pytest.mark.usefixtures('my_method') def test_class_method_usage(self): print('[classMethod]Please follow Testing from WL') # 類直接使用fixture @pytest.mark.usefixtures('my_method') class TestClass2: def test_method_usage_01(self): pass def test_method_usage_02(self): pass
由這段代碼你可以看到,usefixtures 可以被函數、類方法,以及類調用。
(3)、fixture 多參數使用
上述使用方式實現了使不同的測試函數調用不同的測試 fixtures,那么如果我們 fixture 帶參數該怎么辦呢?請看如下代碼:
import pytest @pytest.fixture(params=['hello', 'Testing']) def my_method(request): return request.param def test_use_fixtures_01(my_method): print('this is the first test') print(my_method) @pytest.mark.usefixtures('my_method') def test_use_fixtures_02(): print('this is the second test') # 注意,如果在這里想通過print(my_mthod)來打印出fixuture提供的參數,是不行的, 因為使用usefixtures無法獲取fixture的返回值,如需要fixture的返回值,則需用test_use_fixtures_01那樣的調用方式
執行這段代碼,將會看到有4條測試用例被執行。由此可見,pytest 通過 fixture 和其參數 params 實現了數據驅動。
(4)、通過 autouse 參數隱式使用
以上方式實現了 fixtures 和測試函數的松耦合,但是仍然存在問題:每個測試函數都需要顯式聲明要用哪個 fixtures。
基於此,pytest 提供了autouse 參數,允許我們在不調用 fixture 裝飾器的情況下使用定義的fixture,請看下面的例子:
#test_fixture_usage.py import pytest @pytest.fixture(params=['hello', 'Testing'], autouse=True, ids=['test1', 'test2'], name='test') def my_method(request): print(request.param) def test_use_fixtures_01(): print('this is the first test') def test_use_fixtures_02(): print('this is the second test')
通過運行上段代碼,並使用 allure[allure如何生成測試報告推文鏈接] 生成測試報告的結果如下:
當定義了 fixture 函數,並且 autouse 為 True 時,無須顯式的在測試函數中聲明要使用 fixture(在本例中,你看不到 my_method 這個 fixture 在測試方法中被顯式調用)。定義的 fixture 將在 pytest.fixtures 指定的范圍內,對其下的每一個測試函數都應用 fixture。
在本例中,scope 參數沒有定義,將使用默認值“function”, 即每一個測試函數都會執行, 而我們的 params 又提供了兩組參數,所以共 4 條測試用例被執行。
請注意下測試用例名稱,針對每一個測試用例,因為在@pytest.fixture 指定了 ids 為 ['test1', 'test2'], 故測試用例名中也包括了指定的 id。
(5)、多 fixture 笛卡爾積使用
當你有多個 fixture 需要疊加使用時, 可以疊加使用。注意:此方式將把 fixure 的各組參數以笛卡爾積的形式組織,以下列代碼為例,執行將生成 4 條測試用例。
import pytest class TestClass: @pytest.fixture(params=['hello', 'Testing'], autouse=True) def my_method1(self, request): print('the param are:{}'.format(request.param)) return request.param @pytest.fixture(params=['world', 'is good'], autouse=True) def my_method2(self, request): print('the param are:{}'.format(request.param)) return request.param def test_use_fixtures_01(self): pass
(6)、使用 conftest.py 來共享 fixture
通過上面的舉例學習,大家應該掌握了如何在同一個文件中進行 fixture 的定義、共享和使用。但在日常工作測試中,我們常常需要在全局范圍內使用同一個測試前置操作。
例如:測試開始時首先進行登錄操作,接着連接數據庫等操作。
這種情況下,我們就需要使用 conftest.py。在 conftest.py 中定義的 fixture 不需要進行 import,pytest 會自動查找使用。pytest 查找 fixture 的順序是首先查找測試類(Class),接着查找測試模塊(Module),然后是 conftest.py 文件,最后是內置或者第三方插件。
下面來看下如何使用 conftest.py
假設目錄結構如下:
|--APITest |--tests |--test_fixture1.py |--test_baidu_fixture_sample.py |--conftest.py |--__init__.py
conftest.py 的代碼如下:
# conftest.py import pytest import requests from selenium import webdriver @pytest.fixture(scope="session") # 此方法名可以是你登錄的業務代碼,也可以是其他,這里暫命名為login def login(): driver = webdriver.Chrome() driver.implicitly_wait(30) base_url = "http://www.baidu.com/" s = requests.Session() yield driver, s, base_url print('turn off browser driver') driver.quit() print('turn off requests driver') s.close() @pytest.fixture(scope="function", autouse=True) def connect_db(): print('connecting db') # 此處寫你的連接db的業務邏輯 pass
test_fixture1.py 的代碼如下:
# test_fixture1.py import pytest class TestClass: def test_use_fixtures_01(self, login): print('I am data:{}'.format(login))
test_baidu_fixture_sample.py 的代碼如下:
import time import pytest @pytest.mark.baidu class TestBaidu: @pytest.mark.parametrize('search_string, expect_string', [('Testing', 'Testing'), ('helloworld.com', 'Testing')]) def test_baidu_search(self, login, search_string, expect_string): driver, s, base_url = login driver.get(base_url + "/") driver.find_element_by_id("kw").send_keys(search_string) driver.find_element_by_id("su").click() time.sleep(2) search_results = driver.find_element_by_xpath('//*[@id="1"]/h3/a').get_attribute('innerHTML') print(search_results) assert (expect_string in search_results) is True if __name__ == "__main__": pytest.main([])
在命令行中通過如下代碼執行:
D:\Auto\APITest>pytest -s -q --tb=no tests --alluredir=./allure_reports
測試執行完成后,查看執行結果:
從上圖中可以注意到,connecting db 這條語句被打印了三次,是因為在 conftest.py 里把 connect_db 這個 fixture 的 scope 設置為 function 且 autouse 的屬性值是 True。而 turn off browser driver,turn off requests driver 這兩條語句僅僅執行了一次,是因為 login 這個 fixture 的 scope 是 session,故它在整個 session 中僅僅執行了一次。
另外請注意下在 fixture login 中,有如下的語句:
... ... yield driver, s, base_url print('turn off browser driver') driver.quit() print('turn off requests driver') s.close()
這個是什么意思呢?在 pytest 的 fixture 里,yield關鍵字語句之前的屬於 set up,而 yield 以后的語句屬於 tear down。
這樣你就明白了,為什么以下語句是最后執行的了:
print('turn off browser driver') driver.quit() print('turn off requests driver') s.close()
pytest.mark.parametrize 和 pytest.fixture 結合使用
通過上面的講解我們了解到,在 pytest 中可以使用 pytest.mark.parametrize 裝飾器進行數據驅動測試,可以使用 pytest.fixture 裝飾器進行測試的 setup、teardown,以及 fixture 共享的測試。
當 pytest.mark.parametrize 和 pytest.fixture 結合起來,能起到什么效果呢?
(1)、減少了重復代碼,實現了代碼全局共享
所有的測試前置及后置功能均可以定義在 conftest.py 文件中,供整個測試使用,而不必在每一個測試類中定義。這樣做大大減少了重復代碼,且 conftest.py 定義在項目根目錄,就可以應用在全局,定義在某一個文件夾,就可以應用於這個文件夾下的所有測試文件。
(2)、可以使測試僅關注測試自身
測試僅圍繞自身業務進行編碼即可,配合使用 conftest.py 及 pytest.fixture 可實現,在一個測試類中,僅僅包括測試自身的代碼,而不必考慮測試前的准備以及測試后的清理工作。
(3)、框架遷移更容易
如果是 UI 自動化測試,可在 conftest.py 文件中包括 Web Driver 的所有操作,如果是 API 測試,可在 conftest.py 文件中編寫所有接口請求操作。這樣當新項目需要應用自動化框架時,僅需更改 tests 文件夾下的測試用例即可。
pytest.mark.parametrize 和 pytest.fixture 結合示例:
# test_sample.py import pytest @pytest.fixture() def is_odd(request): print('Now the parameter are:--{}\n'.format(request.param)) if int(request.param) % 2 == 0: return False else: return True @pytest.mark.parametrize("is_odd", [1, 0], indirect=True) def test_is_odd(is_odd): if is_odd: print("is odd number") else: print("is not odd number") if __name__ == "__main__": pytest.main([])
上述代碼定義了一個 fixture 方法 is_odd 和一個數據驅動的方法 test_is_odd。其中,fixture 方法 is_odd 判斷一個數是否是奇數;而數據驅動的方法 test_is_odd 會提供一組數據,並且調用 is_odd 這個 fixture 進行判斷。
總結
今天的分享內容是 pytest 測試框架如何進行數據驅動,可以通過結合使用 pytest.mark.parametrize 和 pytest.fixture 裝飾器。如果你學會了 pytest.mark.parametrize 和 pytest.fixture 的各種用法,對你的測試框架將可以運用自如。
歡迎關注【無量測試之道】公眾號,回復【領取資源】
Python編程學習資源干貨、
Python+Appium框架APP的UI自動化、
Python+Selenium框架Web的UI自動化、
Python+Unittest框架API自動化、
資源和代碼 免費送啦~
文章下方有公眾號二維碼,可直接微信掃一掃關注即可。
備注:我的個人公眾號已正式開通,致力於測試技術的分享,包含:大數據測試、功能測試,測試開發,API接口自動化、測試運維、UI自動化測試等,微信搜索公眾號:“無量測試之道”,或掃描下方二維碼:
添加關注,讓我們一起共同成長!