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 創建測試用例
- 創建
test_
開頭的python文件 - 編寫
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個部分:
yield
之前的代碼,在測試前執行,對應xUnit中setUPyield
之后的代碼,在測試后執行,對應xUnit中yeadDown
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 標記
-
選擇復雜的標記
pytest -m "標記A and 標記B"
同時具有標記A 和標記B的用例pytest -m "標記A or 標記B"
具有標記A 或標記B 的用例pytest -m "not 標記A "
不具有標記A 的B用例
@pytest.mark.mmm
@pytest.mark.sanmu
def test_sanmu():
pass
@pytest.mark.mmm
@pytest.mark.yiran
def test_yiran():
pass
注冊自定義標記:pytest知道哪些自定義標記是正確的,就不會發出警告
# pytest.ini
[pytest]
markers =
mmm
sanmu
yiran
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", "sanmu")
config.addinivalue_line("markers", "yiran")
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
,當前目錄中的模塊可以被導入