5 分鐘快速上手 pytest 測試框架
為什么要做單元測試
print() 函數(shù)將其打印輸出到控制臺上。def myfunc(*args, **kwargs):
do_something()
data = ...
print(data)
print() 函數(shù)來確保結果的準確性,但同時,也由于要測試的模塊或者函數(shù)變多,代碼中也會逐漸遺留著各種未被去掉或注釋的 print() 調(diào)用,讓整個代碼變得不是那么簡潔得體。print() 進行測試的步驟統(tǒng)一地放到單元測試中來進行。unittest。但對于新手來說,unittest 在學習曲線上是稍微有點難度的,因為是需要通過繼承測試用例類(TestCase)來進行封裝,所以需要對面向對象的知識有足夠多的了解;而和類綁定在一起就意味著如果想要實現(xiàn)定制化或者模塊解耦,可能就需要多花一些時間在設計劃分上。
TestCase 來實現(xiàn)我們的測試,我們只需要簡單到保持我們原有的代碼邏輯不變,外加一個 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 里的內(nèi)容是什么。快速實現(xiàn)你的第一個 Pytest 測試
pip install pytest 安裝 pytest 之后,我們就可以快速實現(xiàn)我們的第一個測試。test_main.py 命名,然后當中留存如下內(nèi)容: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 已經(jīng)幫我們將待測試的參數(shù)傳入到測試函數(shù)中,并實現(xiàn)對應的結果:
for 循環(huán)傳參,并且還能直觀地從結果中看到每次測試中傳入?yún)?shù)的具體數(shù)值是怎樣。這里我們只通過 pytest 提供的 mark.parametrize 裝飾器就搞定了。也說明 pytest 的上手程度是比較容易的,只不過我們需要稍微了解一下這個框架中的一些概念。Pytest 概念與用法
命名
test_* 開頭或是以 *_test 結尾,這是為了遵守標準的測試約定。如果我們將前面快速上手的例子文件名中的 test_ 去掉,就會發(fā)現(xiàn) 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
pytest test.py::test_demo
pytest test.py::TestDemo::test_demo
標記(mark)
mark 標記是一個十分好用的功能,通過標記的裝飾器來裝飾我們的待測試對象,讓 pytest 在測試時會根據(jù) mark 的功能對我們的函數(shù)進行相應的操作。mark 功能,我們只挑常用的說。參數(shù)測試:pytest.parametrize
mark.parametrize 主要就是用于我們想傳遞不同參數(shù)或不同組合的參數(shù)到一個待測試對象上的這種場景。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 會有一些差異:前者主要是模擬不同參數(shù)下時待測對象會輸出怎樣的結果,而后者是在固定參數(shù)或數(shù)據(jù)的情況下,去測試會得到怎樣的結果。跳過測試
add(),但當版本大于 Python 3.3 時使用必然會出現(xiàn)問題。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,大于則會自動跳過。預期異常
def div(x, y):
return x / y
y=0 時,必然會引發(fā) ZeroDivisionError 異常。所以通常的做法要么就用 try...exception 來捕獲異常,并且拋出對應的報錯信息(我們也可以使用 if 語句進行條件判斷,最后也同樣是拋出報錯):def div(x, y):
try:
return x/y
except ZeroDivisionError:
raise ValueError("y 不能為 0")
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
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 這一行裝飾器代碼,通過該裝飾器我們可以直接使用一個帶有資源的函數(shù)將其作為我們的載具,在使用時將函數(shù)的簽名(即命名)作為參數(shù)傳入到我們的測試用例中,在運行測試時 pytest 則會自動幫助我們進行注入。
myclient() 中 db 對象的 connect() 方法調(diào)用模擬數(shù)據(jù)庫連接的方法,在測試完成之后會再次幫我們調(diào)用 close() 方法釋放資源。fixture 機制是一個讓我們能實現(xiàn)復雜測試的關鍵,試想我們以后只需要寫好一個帶有測試數(shù)據(jù)的 fixture,就可以在不同的模塊、函數(shù)或者方法中多次使用,真正做到「一次生成,處處使用」。function:函數(shù)作用域(默認)class:類作用域module:模塊作用域package:包作用域session:會話作用域
@pytest.fixture() 中多增加一個 scope 參數(shù),從而提升載具作用的范圍。conftest.py 文件中進行統(tǒng)一管理:# 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 擴展
pip 命令安裝即可,最后使用只需要簡單的參照插件的使用文檔編寫相應的部分,最后啟動 pytest 測試即可。pytest-xdist
-n <CPU_NUMBER> 參數(shù)即可,其中的 CPU 數(shù)量可以直接用 auto 代替,它會自動幫你調(diào)整 pytest 測試所使用的 CPU 核心數(shù):
pytest-asyncio
@pytest.mark.asyncio 標記裝飾異步函數(shù)或方法,然后進行測試即可: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
結語
評論
圖片
表情
