您好,登錄后才能下訂單哦!
這篇文章主要講解了“如何使用Pytest測試框架”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“如何使用Pytest測試框架”吧!
為什么要做單元測試
相信很多 Python 使用者都會有這么一個經歷,為了測試某個模塊或者某個函數是否輸出自己預期的結果,往往會對產出結果的部分使用 print() 函數將其打印輸出到控制臺上。
def myfunc(*args, **kwargs): do_something() data = ... print(data)
在一次次改進過程中會不得不經常性地使用 print() 函數來確保結果的準確性,但同時,也由于要測試的模塊或者函數變多,代碼中也會逐漸遺留著各種未被去掉或注釋的 print() 調用,讓整個代碼變得不是那么簡潔得體。
在編程中往往會存在「單元測試」這么一個概念,即指對軟件中的最小可測試單元進行檢查和驗證。這個最小可測單元可以是我們的表達式、函數、類、模塊、包中的任意一種或組合,因此我們可以將使用 print() 進行測試的步驟統一地放到單元測試中來進行。
在 Python 中官方早已經為我們內置好了用以進行單元測試的模塊 unittest。但對于新手來說,unittest 在學習曲線上是稍微有點難度的,因為是需要通過繼承測試用例類(TestCase)來進行封裝,所以需要對面向對象的知識有足夠多的了解;而和類綁定在一起就意味著如果想要實現定制化或者模塊解耦,可能就需要多花一些時間在設計劃分上。
所以,為了能讓測試變得簡單且具備可擴展性,一個名為 pytest 的測試框架在 Python 社區中誕生了,使用 pytest 我們可以不用考慮如何基于 TestCase 來實現我們的測試,我們只需要簡單到保持我們原有的代碼邏輯不變,外加一個 assert 關鍵字來斷言結果,剩下的部分 pytest 會幫我們處理。
# main.py import pytest raw_data = read_data(...) def test_myfunc(*args, **kwargs): do_something() data = ... assert data == raw_data if __name__ == '__main__': pytest.main()
之后我們只需要運行包含上述代碼的 main.py 文件,就能在終端控制臺上看到 pytest 為我們測試得到的結果。如果結果通過,則不會有過多的信息顯示,如果測試失敗,則會拋出錯誤信息并告知運行時 data 里的內容是什么。
盡管說 pytest 已經足夠簡單,但它也提供了許多實用的功能(如:依賴注入),這些功能本身是存在著一些概念層面的知識;但這并不意味著勸退想要使用 pytest 來測試自己代碼的人,而是讓我們擁有更多的選擇,因此只有對 pytest 的這些功能及其概念有了更好地了解,我們才能夠充分發揮 pytest 的威力。
快速實現你的第一個 Pytest 測試
通過 pip install pytest 安裝 pytest 之后,我們就可以快速實現我們的第一個測試。
首先我們可以任意新建一個 Python 文件,這里我直接以 test_main.py 命名,然后當中留存如下內容:
from typing import Union import pytest def add( x: Union[int, float], y: Union[int, float], ) -> Union[int, float]: return x + y @pytest.mark.parametrize( argnames="x,y,result", argvalues=[ (1,1,2), (2,4,6), (3.3,3,6.3), ] ) def test_add( x: Union[int, float], y: Union[int, float], result: Union[int, float], ): assert add(x, y) == result
之后將終端切換到該文件所處路徑下,然后運行 pytest -v,就會看到 pytest 已經幫我們將待測試的參數傳入到測試函數中,并實現對應的結果:
可以看到我們無需重復地用 for 循環傳參,并且還能直觀地從結果中看到每次測試中傳入參數的具體數值是怎樣。這里我們只通過 pytest 提供的 mark.parametrize 裝飾器就搞定了。也說明 pytest 的上手程度是比較容易的,只不過我們需要稍微了解一下這個框架中的一些概念。
命名
如果需要 pytest 對你的代碼進行測試,首先我們需要將待測試的函數、類、方法、模塊甚至是代碼文件,默認都是以 test_* 開頭或是以 *_test 結尾,這是為了遵守標準的測試約定。如果我們將前面快速上手的例子文件名中的 test_ 去掉,就會發現 pytest 沒有收集到對應的測試用例。
當然我們也可以在 pytest 的配置文件中修改不同的前綴或后綴名,就像官方給出的示例這樣:
# content of pytest.ini # Example 1: have pytest look for "check" instead of "test" [pytest] python_files = check_*.py python_classes = Check python_functions = *_check
但通常情況下我們使用默認的 test 前后綴即可。如果我們只想挑選特定的測試用例或者只對特定模塊下的模塊進測試,那么我們可以在命令行中通過雙冒號的形式進行指定,就像這樣:
pytest test.py::test_demo pytest test.py::TestDemo::test_demo
在 pytest 中,mark 標記是一個十分好用的功能,通過標記的裝飾器來裝飾我們的待測試對象,讓 pytest 在測試時會根據 mark 的功能對我們的函數進行相應的操作。
官方本身提供了一些預置的 mark 功能,我們只挑常用的說。
正如前面的示例以及它的命名意思一樣,mark.parametrize 主要就是用于我們想傳遞不同參數或不同組合的參數到一個待測試對象上的這種場景。
正如我們前面的 test_add() 示例一樣,分別測試了:
當 x=1 且 y=1 時,結果是否為 result=2 的情況
當 x=2 且 y=4 時,結果是否為 result=6 的情況
當 x=3.3 且 y=3 時,結果是否為 result=6.3 的情況
……
我們也可以將參數堆疊起來進行組合,但效果也是類似:
import pytest @pytest.mark.parametrize("x", [0, 1]) @pytest.mark.parametrize("y", [2, 3]) @pytest.mark.parametrize("result", [2, 4]) def test_add(x, y, result): assert add(x,y) == result
當然如果我們有足夠多的參數,只要寫進了 parametrize 中,pytest 依舊能幫我們把所有情況都給測試一遍。這樣我們就再也不用寫多余的代碼。
但需要注意的是,parametrize 和我們后面將要講到的一個重要的概念 fixture 會有一些差異:前者主要是模擬不同參數下時待測對象會輸出怎樣的結果,而后者是在固定參數或數據的情況下,去測試會得到怎樣的結果。
跳過測試
有些情況下我們的代碼包含了針對不同情況、版本或兼容性的部分,那么這些代碼通常只有在符合了特定條件下可能才適用,否則執行就會有問題,但產生的這個問題的原因不在于代碼邏輯,而是因為系統或版本信息所導致,那如果此時作為用例測試或測試失敗顯然不合理。比如我針對 Python 3.3 版本寫了一個兼容性的函數,add(),但當版本大于 Python 3.3 時使用必然會出現問題。
因此為了適應這種情況 pytest 就提供了 mark.skip 和 mark.skipif 兩個標記,當然后者用的更多一些。
import pytest import sys @pytest.mark.skipif(sys.version_info >= (3,3)) def test_add(x, y, result): assert add(x,y) == result
所以當我們加上這一標記之后,每次在測試用例之前使用 sys 模塊判斷 Python 解釋器的版本是否大于 3.3,大于則會自動跳過。
預期異常
代碼只要是人寫的必然會存在不可避免的 BUG,當然有一些 BUG 我們作為寫代碼的人是可以預期得到的,這類特殊的 BUG 通常也叫異常(Exception)。比如我們有一個除法函數:
def div(x, y): return x / y
但根據我們的運算法則可以知道,除數不能為 0;因此如果我們傳遞 y=0 時,必然會引發 ZeroDivisionError 異常。所以通常的做法要么就用 try...exception 來捕獲異常,并且拋出對應的報錯信息(我們也可以使用 if 語句進行條件判斷,最后也同樣是拋出報錯):
def div(x, y): try: return x/y except ZeroDivisionError: raise ValueError("y 不能為 0")
因此,此時在測試過程中,如果我們想測試異常斷言是否能被正確拋出,此時就可以使用 pytest 提供的 raises() 方法:
import pytest @pytest.mark.parametrize("x", [1]) @pytest.mark.parametrize("y", [0]) def test_div(x, y): with pytest.raises(ValueError): div(x, y)
這里需要注意,我們需要斷言捕獲的是引發 ZeroDivisionError 后我們自己指定拋出的 ValueError,而非前者。當然我們可以使用另外一個標記化的方法(pytest.mark.xfail)來和 pytest.mark.parametrize 相結合:
@pytest.mark.parametrize( "x,y,result", [ pytest.param(1,0, None, marks=pytest.mark.xfail(raises=(ValueError))), ] ) def test_div_with_xfail(x, y, result): assert div(x,y) == result
這樣測試過程中會直接標記出失敗的部分。
Fixture
在 pytest 的眾多特性中,最令人感到驚艷的就是 fixture。關于 fixture 的翻譯大部分人都直接將其直譯為了「夾具」一詞,但如果你有了解過 Java Spring 框架的 那么你在實際使用中你就會更容易將其理解為 IoC 容器類似的東西,但我自己認為它叫「載具」或許更合適。
因為通常情況下都是 fixture 的作用往往就是為我們的測試用例提供一個固定的、可被自由拆裝的通用對象,本身就像容器一樣承載了一些東西在里面;讓我們使用它進行我們的單元測試時,pytest 會自動向載具中注入對應的對象。
這里我稍微模擬了一下我們在使用使用數據庫時的情況。通常我們會通過一個數據庫類創建一下數據庫對象,然后使用前先進行連接 connect(),接著進行操作,最后使用完之后斷開連接 close() 以釋放資源。
# test_fixture.py import pytest class Database(object): def __init__(self, database): self.database = database def connect(self): print(f"\n{self.database} database has been connected\n") def close(self): print(f"\n{self.database} database has been closed\n") def add(self, data): print(f"`{data}` has been add to database.") return True @pytest.fixture def myclient(): db = Database("mysql") db.connect() yield db db.close() def test_foo(myclient): assert myclient.add(1) == True
在這段代碼中,實現載具的關鍵是 @pytest.fixture 這一行裝飾器代碼,通過該裝飾器我們可以直接使用一個帶有資源的函數將其作為我們的載具,在使用時將函數的簽名(即命名)作為參數傳入到我們的測試用例中,在運行測試時 pytest 則會自動幫助我們進行注入。
在注入的過程中 pytest 會幫我們執行 myclient() 中 db 對象的 connect() 方法調用模擬數據庫連接的方法,在測試完成之后會再次幫我們調用 close() 方法釋放資源。
pytest 的 fixture 機制是一個讓我們能實現復雜測試的關鍵,試想我們以后只需要寫好一個帶有測試數據的 fixture,就可以在不同的模塊、函數或者方法中多次使用,真正做到「一次生成,處處使用」。
當然 pytest 給我們提供了可調節載具作用域(scope)的情況,從小到大依次是:
function:函數作用域(默認)
class:類作用域
module:模塊作用域
package:包作用域
session:會話作用域
載具會隨著作用域的生命周期而誕生、銷毀。所以如果我們希望創建的載具作用域范圍增加,就可以在 @pytest.fixture() 中多增加一個 scope 參數,從而提升載具作用的范圍。
雖然 pytest 官方為我們提供了一些內置的通用載具,但通常情況下我們自己自定義的載具會更多一些。所以我們都可以將其放到一個名為 conftest.py 文件中進行統一管理:
# conftest.py import pytest class Database: def __init__(self, database): self.database:str = database def connect(self): print(f"\n{self.database} database has been connected\n") def close(self): print(f"\n{self.database} database has been closed\n") def add(self, data): print(f"\n`{data}` has been add to database.") return True @pytest.fixture(scope="package") def myclient(): db = Database("mysql") db.connect() yield db db.close()
因為我們聲明了作用域為同一個包,那么在同一個包下我們再將前面的 test_add() 測試部分稍微修改一下,無需顯式導入 myclient 載具就可以直接注入并使用:
from typing import Union import pytest def add( x: Union[int, float], y: Union[int, float], ) -> Union[int, float]: return x + y @pytest.mark.parametrize( argnames="x,y,result", argvalues=[ (1,1,2), (2,4,6), ] ) def test_add( x: Union[int, float], y: Union[int, float], result: Union[int, float], myclient ): assert myclient.add(x) == True assert add(x, y) == result
之后運行 pytest -vs 即可看到輸出的結果:
Pytest 擴展
對于每個使用框架的人都知道,框架生態的好壞會間接影響框架的發展(比如 Django 和 Flask)。而 pytest 預留了足夠多的擴展空間,加之許多易用的特性,也讓使用 pytest 存在了眾多插件或第三方擴展的可能。
根據官方插件列表所統計,目前 pytest 有多大 850 個左右的插件或第三方擴展,我們可以在 pytest 官方的 Reference 中找到 Plugin List 這一頁面查看,這里我主要只挑兩個和我們下一章實踐相關的插件:
相關插件我們可以根據需要然后通過 pip 命令安裝即可,最后使用只需要簡單的參照插件的使用文檔編寫相應的部分,最后啟動 pytest 測試即可。
pytest-xdist
pytest-xdist 是一個由 pytest 團隊維護,并能讓我們進行并行測試以提高我們測試效率的 pytest 插件,因為如果我們的項目是有一定規模,那么測試的部分必然會很多。而由于 pytest 收集測試用例時是以一種同步的方式進行,因此無法充分利用到多核。
因此通過 pytest-xdist 我們就能大大加快每輪測試的速度。當然我們只需要在啟動 pytest 測試時加上 -n
pytest-asyncio
pytest-asycnio 是一個讓 pytest 能夠測試異步函數或方法的擴展插件,同樣是由 pytest 官方維護。由于目前大部分的異步框架或庫往往都是會基于 Python 官方的 asyncio 來實現,因此 pytest-asyncio 可以進一步在測試用例中集成異步測試和異步載具。
我們直接在測試的函數或方法中直接使用 @pytest.mark.asyncio 標記裝飾異步函數或方法,然后進行測試即可:
import asyncio import pytest async def foo(): await asyncio.sleep(1) return 1 @pytest.mark.asyncio async def test_foo(): r = await foo() assert r == 1
感謝各位的閱讀,以上就是“如何使用Pytest測試框架”的內容了,經過本文的學習后,相信大家對如何使用Pytest測試框架這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。