大家好,我是玉米君,本篇文章将從基礎、斷言、夾具、标記、配置、插件和布局幾個方面帶大家了解pytest測試架構,并配置腳本案例,讓你熟悉起來更友善。
1. pytest特點和基本用法
Python内置了測試架構unit test,但是了解units同學知道它是一個擁有濃烈的Java風格,比如說類名、方法名字會使用駝峰,而且必須要繼承父類才能的定義測試用例等等。
那有一些Python開發者,他覺得這種方式這種風格不太适應,是以做了一個更加pythonic的測試架構,最開始隻是工具箱的一部分(py.test),後來這個測試架構獨立出來的就成為了大名鼎鼎的pytest。
1.1 安裝pytest
使用pip進行安裝
pip install pytest -U
驗證安裝
pytest
pytest --version
pytest -h
1.2 建立測試用例
- 建立
開頭的python檔案test_
- 編寫
開頭的函數test_
- 在函數中使用
關鍵字assert
# test_main.py
def test_sanmu():
a = 1
b = 2
assert a == b
1.3 執行測試用例
- 自動執行所有的用例
-
pytest
-
- 執行指定檔案中所有用例
-
pytest filename.py
-
- 執行指定檔案夾中的所有檔案中的所有用例
-
pyest dirname
-
- 執行指定的用例
-
pytest test_a.py::test_b
-
測試發現:搜集用例
一般規則:
- 從目前目錄開始,周遊每一個子目錄 (不論這個目錄是不是包)
- 在目錄搜尋
和test_*.py
,并導入(測試檔案中的代碼會自動執行)*_test.py
- 在導入的子產品手機以下特征的對象,作為測試用例
-
開頭的函數test
-
開頭類及其Test
開頭方法 (這個類不應該有test
)__init__
- unittest架構的測試用例
-
慣例(約定):和測試相關的一切,用
test
或者
test_
開頭
1.4 讀懂測試結果
import pytest
def test_ok():
print("ok")
def test_fail():
a, b = 1, 2
assert a == b
def test_error(something):
pass
@pytest.mark.xfail(reason="always xfail")
def test_xpass():
pass
@pytest.mark.xfail(reason="always xfail")
def test_xfail():
assert False
@pytest.mark.skip(reason="skip is case")
def test_skip():
pass
pytest報告分為幾個基本部分:
- 報告頭
- 用例收集情況
- 執行狀态
- 用例的結果
- 進度
- 資訊
- 錯誤資訊
- 統計資訊
- 耗時資訊
報告中的結果縮寫符合是什麼含義
符号 | 含義 |
---|---|
. | 測試通過 |
F | 測試失敗 |
E | 出錯 |
X | XPass 預期外的通過 |
x | xfailed 預期失敗 |
s | 跳過用例 |
如果要展示更加詳細的結果,可以通過參數的方式設定
pytest -vrA
2. 斷言
2.1 普通斷言
pytest使用python内置關鍵字
assert
驗證預期值和實際值
def test_b():
a = 1
b = 2
assert a == b
pytest 和python處理方式不一樣:
- 數值比較:會顯示具體數值
2.2 容器型資料斷言
如果是兩個容器型資料(字元串、元組、清單、字典、數組),斷言失敗,會将兩個資料進行diff比較,找出不用
def test_b():
a = [1, 1, 1, 1, 1, 1, 1, 1, 1, 0]
b = [1, 1, 1, 1, 1, 1, 1, 1, 1, 2]
assert a == b, "a和b不相等"
> assert a == b, "a和b不相等"
E AssertionError: a和b不相等
E assert [1, 1, 1, 1, 1, 1, ...] == [1, 1, 1, 1, 1, 1, ...]
E At index 9 diff: 0 != 2
E Full diff:
E - [1, 1, 1, 1, 1, 1, 1, 1, 1, 2]
E ? ^
E + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0]
E ?
2.3 斷言出現異常
一般情況,:
- 執行測試用例出現了異常,認為失敗
- 如果沒有出現異常,認為通過。
“斷言出現異常” :
- 出現了異常,認為通過
- 沒有出現異常,認為失敗
def test_b():
with pytest.raises(ZeroDivisionError):
1 / 0
不僅可以斷言出現了異常,還可以斷言出現什麼異常,更可以斷言誰引發的異常
def test_b():
d = dict()
with pytest.raises(KeyError) as exc_info:
print(d["a"]) # 這行代碼,預期不發生異常
print(d["b"]) # 這行代碼,預期異常
assert "b" in str(exc_info.value)
2.4 斷言出現警告
警告(Warning)是Exception的子類,但是它不是有
raise
關鍵字抛出,而是通過
warnings.warn
函數進行執行。
def f():
pass
warnings.warn("再過幾天,就要放假了", DeprecationWarning)
def test_b():
with pytest.warns(DeprecationWarning):
f()
3. 夾具
單元代碼?
建立測試用例:
- 建立test_開頭的函數
- 在函數内使用斷言關鍵字
一個測試用例的執行分為四個步驟:
- 預置條件
- 執行動作
- 斷言結果
- 清理現場
為了重複測試結果不會異常,也為了不會幹擾其他用例。
在理想情況,為了突出用例重點,用例中應該隻有2(執行動作)和3(斷言結果)
- 1 和4 應當封裝起來
- 1 和4 能夠自動執行
夾具(Fixture)是軟體測試裝置,作用和目的:
- 在測試開始前,準備好相關的測試環境
- 在測試結束後,銷毀相關的内容
以socket聊天伺服器作為例子,示範夾具的用法
socket服務的測試步驟:
- 建立socket連接配接
- 利用socket執行測試動作
- 對結果進行斷言
- 斷開socket
3.1 建立夾具
3.1.1 快速上手
夾具的特性:
- 在測試用例之前執行
- 具體重複性和必要性
夾具client:自動和server建立socket連接配接,并供用例使用
建立一個函數,并使用
@pytest.fixture()
裝飾器即可
@pytest.fixture()
def client():
client = socket.create_connection(server_address, 1)
return client
3.1.2 setup 和 teardwon
pytest 有2種方式實作teardwon,這裡隻推薦一種: 使用yield關鍵字
函數中有了
yield
關鍵字之後,成了生成器,可以多次調用
@pytest.fixture()
def server():
p = Process(target=run_server, args=(server_address,))
p.start() # 啟動服務端
print("啟動服務端")
yield p
p.kill()
yield
關鍵字 使夾具執行分為2個部分:
-
之前的代碼,在測試前執行,對應xUnit中setUPyield
-
之後的代碼,在測試後執行,對應xUnit中yeadDownyield
3.1.3 夾具範圍
夾具生命周期:
- 被需要用的時候建立
- 在結束範圍的時候銷毀
- 如果夾具存在,不會重複建立
pytest夾具範圍有5種:
- function:預設的範圍,夾具在單個用例結束的時候被銷毀
- class: 夾具在類的最後一個用例結束的時候被銷毀
- module:夾具在子產品的最後一個用例結束的時候被銷毀
- package:夾具在包的最後一個用例結束的時候被銷毀
- session:夾具在整個測試活動的最後一個用例結束的時候被銷毀
使用Python,如果完全不會class,是沒有任何問題的。
@pytest.fixture(scope="session")
def server():
p = Process(target=run_server, args=(server_address,))
p.start() # 啟動服務端
print("啟動服務端")
yield p
p.kill()
3.1.4 夾具參數化
夾具的參數,可以通過參數化的方式,為夾具産生多個結果 (産生了多個夾具)
如果測試用例要使用的夾具被參數化了,那麼測試用例得到的夾具結果會有多個,每個夾具都會被使用
測試用例也會執行多次
測試用例,不知道自己被執行了多次,正如它不知道夾具被參數一樣
@pytest.fixture(scope="session", params=[9001, 9002, 9003])
def server(request):
port = request.param
p = Process(target=run_server, args=(("127.0.0.1", port),))
p.start() # 啟動服務端
print("啟動服務端") # *3
yield p
p.kill()
3.2 使用夾具
3.2.1 在用例中使用
3.2.2 在夾具中使用
注意:夾具中使用夾具,必須確定範圍是相容的
例子:夾具A 和夾具B,A範圍是
function
,B的範圍是
session
,A可以使用B ,B不可用使用A
- A在第一個用例結束的時候,被銷毀
- B在所有的用例結束的時候,被銷毀
- A比B先被銷毀
使用實際上依賴的關系:
假設:
- A使用B
- B的setup
- A
- B的tearDown
- B使用A (不可以的)
- 第一個用例結束的時候 A被銷毀,B該怎麼辦?
- A的setUP
- B
- A的tearDown
生命周期短的夾具,才可用使用聲明周期長的夾具
3.2.4 自動使用夾具
在一些代碼品質工具中,未被使用的變量和參數,會被評為低品質。
pytest中,夾具可以聲明自動執行,不需要寫在用例參數清單中了。
@pytest.fixture(scope="function", autouse=True)
def server(request):
port = 9001
p = Process(target=run_server, args=(("127.0.0.1", port),))
p.start() # 啟動服務端
print("啟動服務端") # *3
yield p
p.kill()
4. 标記
預設情況下,pytest執行邏輯:
- 運作所有的測試用例
- 執行用例的時候,出現異常,判斷為測試失敗
- 執行用例的時候,沒有出現異常,判斷為測試通過
标記是給測試用例用的
标記的作用,就是為了改變預設行為:
- userfixtures :在測試用例中使用夾具
- skip:跳過測試用例
- xfail: 預期失敗
- parametrize: 參數化測試,反複,多次執行測試用例
- 自定義标記:提供篩選用例的條件,pytest隻執行部分用例
4.1 userfixtures
@pytest.mark.usefixtures("server",) # 隻能給用例,使用夾具
class TestSocket:
def test_create_client(self, client):
print("用戶端的位址", client.getsockname())
print("服務端的位址", client.getpeername())
def test_send_and_recv(self, client):
data = "hello world\n"
client.sendall(data.encode()) # 将字元串轉為位元組,然後發生
f = client.makefile()
msg = f.readline()
assert data == msg
def test_():
pass
4.2 skip 和 skipif
- skip 無條件跳過
- skipif 有條件跳過
class TestSocket:
@pytest.mark.skip(reason="心情不美麗,不想執行這個測試")
def test_create_client(self, client):
print("用戶端的位址", client.getsockname())
print("服務端的位址", client.getpeername())
def test_send_and_recv(self, client):
data = "hello world\n"
client.sendall(data.encode()) # 将字元串轉為位元組,然後發生
f = client.makefile()
msg = f.readline()
assert data == msg
class TestSocket:
@pytest.mark.skipif(sys.platform.startswith("win"), reason="心情不美麗,不想執行這個測試")
def test_create_client(self, client):
print("用戶端的位址", client.getsockname())
print("服務端的位址", client.getpeername())
4.3 xfail
無參數:無條件預期失敗
有參數condition:有條件預期失敗
有參數run: 預期失敗的時候,不執行測試用例
有參數strict:預期外通過時,認為測試失敗
@pytest.mark.xfail(1 != 1, reason="意料中的失敗", run=False, strict=True)
def test_server_not_run():
"""當服務端未啟動的時候,用戶端應該連接配接失敗"""
my_socket = socket.create_connection(server_address, 1)
4.4 參數化
好處:
- 提供測試覆寫率 1,1 => 2, 1,0=>1, 9999999999,1=>100000000
- 反複測試,驗證測試結果穩定性 1,1 => 2 1,1 => 2 1,1 => 2
本質:同一個測試代碼可以執行多個測試用例
@pytest.mark.parametrize("n", [1, "x"])
def test_server_can_re_content(n):
"""測試伺服器可以被多個用戶端反複連接配接和斷開"""
print(n)
my_socket = socket.create_connection(server_address)
4.5 自定義标記
提供篩選用例的條件,使pytest隻執行部分用例
- 選擇簡單的标記
-
pytest -m 标記
-
- 選擇複雜的标記
-
同時具有标記A 和标記B的用例pytest -m "标記A and 标記B"
-
具有标記A 或标記B 的用例pytest -m "标記A or 标記B"
-
不具有标記A 的B用例pytest -m "not 标記A "
-
@pytest.mark.mmm
@pytest.mark.yumi
def test_sanmu():
pass
@pytest.mark.mmm
@pytest.mark.danny
def test_yiran():
pass
注冊自定義标記:pytest知道哪些自定義标記是正确的,就不會發出警告
# pytest.ini
[pytest]
markers =
mmm
yumi
danny
5. 配置
5.1 配置方法
- 指令行
- 靈活
- 如果有多個選項的話,不友善
- 配置檔案
- 特别适合大量,或者不常修改的選項
-
pytest.ini
-
pyproject.toml
- pytest 6.0+ 支援
- 是PEP标準
- 是未來
- python代碼動态配置
- 太靈活, 意味着容易出錯
- 優先級是最高的
# conftest.py 會被pytest自動加載,适合寫配置資訊
def pytest_configure(config): # 鈎子:pytest會自動發現并運作這個函數
config.addinivalue_line("markers", "mmm")
config.addinivalue_line("markers", "yumi")
config.addinivalue_line("markers", "danny")
5.2 配置項
- 查詢幫助資訊
pytest -h
- 檢視pytest參考文檔 https://docs.pytest.org/en/stable/reference.html#id90
約定大于配置
6. 插件
一般情況,插件是一個python的包,在pypi,使用
pytest-
開頭
不一般的情況,需要把插件的在
confgtest.py
進行啟用
6.1 安裝插件
pip install pytest-html
pip install pytest-httpx # mock httpx
pip install pytest-django # test django
6.2 使用插件
各個插件的使用方法 ,各不相同
參考各插件自己的問題
6.3 禁用插件
pytest -p no:插件名稱
- 包名稱:pytest-html
- 插件名稱 :html
7. 布局
- 如果一個測試檔案,存放在目錄中,那麼執行時,這個目錄成為頂級目錄
- 如果一個測試檔案,存放在包中,那麼執行時,根目錄成為頂級目錄
-
,将目前目錄加入到python -m pytest
,目前目錄中的子產品可以被導入sys.path