參考文章:https://mp.weixin.qq.com/s/k6ZZ42CFZhq20RGuBcOOfQ
unittest是python自帶的單元測試框架,它封裝好了一些校驗返回的結果方法和一些用例執行前的初始化操作,使得單元測試易於開展,因為它的易用性,很多同學也拿它來做功能測試和接口測試,只需簡單開發一些功能(報告,初始化webdriver,或者http請求方法)便可實現。
但自動化測試中我們常常需要根據不同需求挑選部分測試用例運行,並且我們希望用例克服環境不穩定的局限,即運行失敗后自動重新運行一次,如果成功就認為是環境問題導致第一次失敗,還有我們經常希望測試用例可以並發執行等等,這些unittest都做不到或者需要大量二次開發才能做到,那么有沒有更加強大的框架可以替代unittests呢?
pytest是python里的一個強大框架,它可以用來做單元測試,你也可以用來做功能,接口自動化測試。而且它比unittest支持的功能更多更全面。但是pytest在Getstarted里給出的實例卻很簡單,很多同學錯以為它只是跟unittest一樣是個單元測試框架罷了,如果你查詢中文互聯網,你也只能找到寥寥數篇大致一樣的用法,可以說pytest的精髓使用,沒有被大家挖掘出來,如此強大的框架不應該被埋沒,今天我就帶領大家深入pytest使用,共同領略pytest的強大。
1.pytst安裝
pytest不屬於python的標准庫,所以需要安裝才能使用, 安裝方式如下:
| 1 |
pip install -U pytest |
如果你已經安裝有pytest,想查看它的版本號:
| 1 |
pytest --version |
2.你的第一個pytest測試
| 1 2 3 4 5 6 7 8 |
#直接使用官網用例 # content of TesterTalk.py def func(x): return x + 1 def test_answer(): assert func(3) == 5 #在你的terminial里輸入 python -m pytest TesterTalk.py,你將會看到如下信息: |
非常簡單吧, 如果想運行多個用例該如何做呢?
| 1 2 3 4 5 6 7 8 9 10 11 |
# content of TesterTalk.py class TestClass(object): def test_one(self): x = "this" assert 'h' in x def test_two(self): x = "hello" assert hasattr(x, 'check') #把要測試的case以test_開頭就好了。 #terminal里輸入 pytest TesterTalk.py, 執行結果一個成功一個失敗。 |
注意:
(1).如果你想用pytest尋找整個文件夾下的測試用例,那么文件須以test_開頭或者以test結尾。
(2).測試類以Test開頭,並且不能帶有 init 方法。
(3).測試函數以test開頭。
(4).另外,pytest不支持也不打算支持中文路徑,如果項目路徑中有中文會報錯。
好了,pytest的getStarted就結束了, 看了上面的應用方式的確沒覺得它哪里強大。 別着急,我們再來想一想,如果你有個測試框架,你希望如何用這個框架做測試?
3.靈活的指定測試用例運行集。
在unittest框架里,你只能通過suite.addTest(),或者defaultTestLoader.Discover()兩種方法在查找測試用例,對於你不需要的測試用例,只能用@unittest.skip()
來忽略,但做不到不改動代碼變更測試用例集,pytest很好的實現了這一點,它支持如下查找:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# 1. 運行某個package下的所有用例。 pytest test_4. #會運行test_4下面所有的test_*.py 或者*_test.py文件里所有的Test開頭的類(這個類不允許有__init__方法)下面的以test_開頭的方法。 #2. 運行某個文件(module)下的所有用例。 pytest testtalk.py #會查找這個文件下所有的Test開頭的類(這個類不允許有__init__方法)下面的以test_開頭的方法。 #3. 運行包含(或不包含)某個關鍵詞的測試類/方法。 class TestClass(object): def test_one(self): x = "this" assert 'h' in x def test_two(self): x = "hello" assert hasattr(x, 'check') #在terminal里輸入 pytst -k "TestClass and not two" #會運行 test_one這個function但不會與性test_two. #4.根據node ids運行測試用例。 pytest給每個test assign里一個id。 可以用::隔開。 舉例來說: # TesterTalk.py class TestClass(object): def test_one(self): x = "this" assert 'h' in x def test_two(self): x = "hello" assert hasattr(x, 'check') #在terminal里輸入 pytest TesterTalk.py::TestClass 會執行TestClass下的所有用例 #在terminal里輸入 pytest TesterTalk.py::TestClass::test_two, 僅會執行test_two. #5.根據標簽(mark)運行,比如有個測試類,你想在regression時候才運行,另外一個測試類你想在releasae的時候才運行。 #TesterTalk.py @pytest.mark.release class TestClass1(object): def test_one(self): x = "this" assert 'h' in x def test_two(self): x = "hello" assert hasattr(x, 'check')
@pytest.mark.regression class TestClass2(object): def test_one(self): x = "this" assert 'h' in x def test_two(self): x = "hello" assert hasattr(x, 'check') #你只需在terminal里指定標簽就可以了。 pytest TesterTalk.py -m release pytest TesterTalk.py -m regression 是不是比unittest方便太多了,所有的測試類,測試方法,你只需要用@pytest.mark.XXX裝飾就好了。然后一次更改,多次挑選運行! 這樣就完了嗎? NO, No,No, 你如果想幾個標簽一起運行怎么辦?或者你不想某個標簽運行怎么辦? Very simple: pytest TesterTalk.py -m “release or regression" #有release或者regression標簽的都會運行。 pytest TesterTalk.py -m “not regression” # regression標簽的不會運行。 當然了,unittest里支持的skip方法,pytest也支持,具體用法如下: #TesterTalk.py @pytest.mark.skip('skip') class TestClass1(object): def test_one(self): x = "this" assert 'h' in x def test_two(self): x = "hello" assert hasattr(x, 'check') #skip裝飾器可以放在類前面skip掉整個測試類,也可以防止function前面只skip掉這個function。 #對應的還有skipif, 請自己看文檔了解。 |
難道就僅限於此嗎? 其實pytest幫我們實現了更多的高級功能,比如:
4.並發運行測試用例集
關注公眾號TesterTalk,跟我一起關注測試技術
首先,你得安裝個插件:
| 1 |
pip install pytest-parallel #pytest-parallel比pytst-xdist要更好。 |
其次,要注意區這個插件僅僅支持python3.6版本及以上,而且如果你想多進程並發,必須跑在Unix或者Mac機器上,windows環境僅僅支持多線程運行。
運行上需要指定參數:
–workers (optional) X。 多進程運行, X是進程數。 默認值1。
–tests-per-worker (optional) X. 多線程運行, X是每個worker運行的最大並發線程數。 默認值1。
舉例來說:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# TesterTalk.py import pytest @pytest.mark.release class TestClass1(object): def test_one(self): x = "this" assert 'h' in x def test_two(self): x = "hello" assert hasattr(x, 'check') @pytest.mark.regression class TestClass2(object): def test_one(self): x = "this" assert 'h' in x def test_two(self): x = "hello" assert hasattr(x, 'check') #terminal里輸入 pytest TesterTalk.py --workers 2 #指定一個進程並發 #terminal里輸入 pytest TesterTalk.py --workers 2 --test-per-worker 3 #指定2個進程並發,每個進程最多運行3個線程。 |
5.測試報告優化
| 1 2 3 4 5 6 7 8 |
#允許HTML格式報告 #首先還是要安裝: pip install pytest-html Usage: 使用很簡單帶參數 --html=report.html就好。 仍然拿我們剛才舉例: pytest TesterTalk.py --html=./report.html |
生成的結果如下:
有時候,我們需要克服環境問題,讓失敗的用例rerun,有沒有辦法呢?
| 1 2 3 4 5 6 7 8 9 |
#rerun失敗的case #首先還是要安裝: pip install pytest-rerunfailures Usage: --reruns 5 #rerun 5次 ----reruns 5 --reruns-delay 1 # rerun5次, 但是每次rerun前等待1秒。 仍以我們剛才的為例: #terminal里輸入 pytest TesterTalk.py --reruns 1 --html=./report.html |
生成的結果如下:
可以看到,rerun聚合在了報告里。
我們自動化一般用到持續集成,Jenkins里需要junit XML格式的報告,pytest有沒有辦法直接生成?
| 1 2 3 4 5 6 |
無需安裝,直接使用: pytest --junitxml=path 例如: pytest TesterTalk.py --reruns 1 --html=./report.html --junitxml=./xml_format_report.xml 會在本地目錄生成xml_format_report.xml |
這就結束了嗎?還遠呢?數據參數化你了解下?
6.數據參數化
pytest有幾種數據參數化方式:
pytest.fixture(). 不帶參數
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#1.fixture()不帶參數: import pytest @pytest.fixture() def initial_browser(): from selenium import webdriver return webdriver.Chrome() class TestBaidu(): def test_open_baidu(self, initial_browser): initial_browser.get("http://www.baidu.com")
#在terminal里輸入pytest testing/testertalk.py --html=./report.html, 你會看到百度瀏覽器打開了。說明函數間可以傳遞值,我們也可以利用這個來做unittest里setup()的事情。 |
pytest.fixture(), 帶parms參數:
params with @pytest.fixture, a list of values for each of which the fixture function will execute and can access a value via request.param.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#利用fixtures的params
import pytest
@pytest.fixture(params=[{'username':'TesterTalk',"password":1}, {'username':'Test',"password":1}])
def account_provider(request): #request是固定的。
return request.param #request.parm也是固定的。
def test_login(account_provider):
#some operations
print(account_provider)
假設你需要用多個用戶名密碼測試登錄,只需要用fixture()加params就好。
#在terminal里輸入pytest testing/testertalk.py --html=./report.html
report顯示下圖,可以看到test_login被執行了2遍,每次執行帶入的數據不同:
除了直接用pytest.fixture, 還可以這么用:
pytest.mark.usefixtures()
| 1 2 3 4 5 6 7 8 9 10 11 12 13 |
import pytest @pytest.fixture(params=[{'username':'TesterTalk',"password":1}, {'username':'Test',"password":1}]) def account_provider(request): return request.param @pytest.mark.usefixtures("account_provider") def test_login(account_provider): #some operations print(account_provider) #在terminal里輸入pytest testing/testertalk.py --html=./report.html,report跟上面的演示一樣。 注意: 使用fixture標記函數后,函數將默認接入一個request參數,它將包含使用該fixture函數的信息,這使我們可以更加靈活的根據不同的函數來決定創建不同的對象以及釋放函數。 舉例來說userfixtures可以用作setup()和teardown() |
pytest固然強大,這就結束了嗎?還有什么高階的功能嗎?必須的。
7.pytest.mark.parametrize實現數據驅動
| 1 2 3 4 5 6 7 8 9 10 11 |
#直接在測試方法前直到數據源 import pytest @pytest.mark.parametrize("account_provider", [{'username':'TesterTalk',"password":1}, {'username':'Test',"password":1}]) def test_login(account_provider): #some operations print(account_provider) #在terminal里輸入pytest testing/testertalk.py --html=./report.html,就能看到login跑了2次。 |
如果我的數據來自外部文件呢?
| 1 2 3 4 5 6 7 8 9 10 |
import pytest #直接寫函數讀取外部文件生成數據值,注意values返回值是個list values = read_from_excel() @pytest.mark.parametrize('v', values) def test_login(v): #some operations print(v) #在terminal里輸入pytest testing/testertalk.py --html=./report.html,就能看到login跑了2次,結果跟上面一樣。 |
到這里為止,你已經學習了pytst的基礎功能,高階功能,還有什么嗎? 如果你之前的框架是unittest, pytest支持無縫切換, 你不需要改任何代碼。
記得上次直播我分享的unittest實現的自動化框架嗎,我們看看這個page:
這個是unittest實現的測試類,我們之間在terminal里運行
| 1 |
pytet test_baidu.py --html=./report.html ,就是這么簡單,我們看看報告是什么樣子 |
怎么樣,就問你驚喜不驚喜?!
當然,pytest的特色還遠不只與此,我們最后介紹一個高級特性,它允許你在用例運行的整個session里,或者一個module里共享測試數據。
8.作用域(scope)實現數據共享(autouse)
我們知道,fixture,允許你不帶參數運行和帶參數運行, 調用fixture的第三種方式就是使用autouse
fixture decorator一個optional的參數是autouse, 默認設置為False。 當默認為False,就可以選擇用上面兩種方式來試用fixture。 當設置為True時,在一個session內的所有的test都會自動調用這個fixture。 權限大,責任也大,所以用該功能時也要謹慎小心。
舉例來說,我想初始化我的瀏覽器,但是我不想每次測試運行都初始化,怎么辦呢?我可以用scope限制住。
首先要建立一個conftest.py文件:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import pytest from selenium import webdriver @pytest.fixture(scope="session", autouse=True) def my_session_enginie_chrome(): print("I will use session engine -- chrome") browser= webdriver.Chrome() return browser @pytest.fixture(scope="module") def my_module_enginie_firefox(): print("I will use module engine -- firefox") browser = webdriver.Firefox() return browser @pytest.fixture(scope="module") def my_function_enginie_chrome(): print("I will use funciton engine --chrome") browser = webdriver.Firefox() return browser |
其次,寫我們的測試類
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#test_666.py import pytest ##如果autouse fixture被session裝飾,那么它只會初始化一次,不管它在哪里定義的。通常用於全局系統初始化. @pytest.fixture(scope='session', autouse=True) def before(request): print ("session start") def test_browser(my_session_enginie_chrome, before): browser = my_session_enginie_chrome browser.get("https://wwww.baidu.com") assert 1==1 #整個module用,在這個module范圍內可以用。 @pytest.fixture(scope='module', autouse=True) def before(request): print ("before start--%s" % request.module.__name__) def test_browser(my_module_enginie_firefox, before): browser = my_module_enginie_firefox browser.get("https://wwww.baidu.com") assert 1==1 # 每個test都會運行。 @pytest.fixture(scope='function', autouse=True) def before(request): print ("before start--%s" % request.function.__name__) def test_browser(my_function_enginie_chrome, before): browser = my_function_enginie_chrome browser.get("https://wwww.baidu.com") assert 1==1 |
fixture的存在使得我們在編寫測試函數的准備函數、銷毀函數或者多個條件的測試提供了更加靈活的選擇。
autouse的scope含義如下:
autouse fixtures obey the scope= keyword-argument: if an autouse fixture has scope=’session’ it will only be run once, no matter where it is defined. scope=’class’ means it will be run once per class, etc.
if an autouse fixture is defined in a test module, all its test functions automatically use it.
if an autouse fixture is defined in a conftest.py file then all tests in all test modules below its directory will invoke the fixture.
