一、介紹
本篇文章是介紹的是Python 世界中最火的第三方單元測試框架:pytest。
它有如下主要特性:
- assert 斷言失敗時輸出詳細信息(再也不用去記憶
self.assert*
名稱了) - 自動發(fā)現(xiàn)測試模塊和函數(shù)
- 模塊化夾具用以管理各類測試資源
- 對
unittest
完全兼容,對nose
基本兼容 - 非常豐富的插件體系,有超過 315 款第三方插件糯而,社區(qū)繁榮
和前面介紹 unittest
和 nose
一樣,我們將從如下幾個方面介紹 pytest
的特性泊窘。
二熄驼、用例編寫
同 nose
一樣,pytest
支持函數(shù)烘豹、測試類形式的測試用例瓜贾。最大的不同點是,你可以盡情地使用 assert
語句進行斷言携悯,絲毫不用擔心它會在 nose
或 unittest
中產(chǎn)生的缺失詳細上下文信息的問題祭芦。
比如下面的測試示例中,故意使得 test_upper
中斷言不通過:
import pytest
def test_upper():
assert 'foo'.upper() == 'FOO1'
class TestClass:
def test_one(self):
x = "this"
assert "h" in x
def test_two(self):
x = "hello"
with pytest.raises(TypeError):
x + []
而當使用 pytest
去執(zhí)行用例時憔鬼,它會輸出詳細的(且是多種顏色)上下文信息:
=================================== test session starts ===================================
platform darwin -- Python 3.7.1, pytest-4.0.1, py-1.7.0, pluggy-0.8.0
rootdir: /Users/prodesire/projects/tests, inifile:
plugins: cov-2.6.0
collected 3 items
test.py F.. [100%]
======================================== FAILURES =========================================
_______________________________________ test_upper ________________________________________
def test_upper():
> assert 'foo'.upper() == 'FOO1'
E AssertionError: assert 'FOO' == 'FOO1'
E - FOO
E + FOO1
E ? +
test.py:4: AssertionError
=========================== 1 failed, 2 passed in 0.08 seconds ============================
不難看到龟劲,pytest
既輸出了測試代碼上下文,也輸出了被測變量值的信息轴或。相比于 nose
和 unittest
昌跌,pytest
允許用戶使用更簡單的方式編寫測試用例,又能得到一個更豐富和友好的測試結(jié)果照雁。
三蚕愤、用例發(fā)現(xiàn)和執(zhí)行
unittest
和 nose
所支持的用例發(fā)現(xiàn)和執(zhí)行能力,pytest
均支持饺蚊。 pytest
支持用例自動(遞歸)發(fā)現(xiàn):
- 默認發(fā)現(xiàn)當前目錄下所有符合
test_*.py
或*_test.py
的測試用例文件中萍诱,以test
開頭的測試函數(shù)或以Test
開頭的測試類中的以test
開頭的測試方法 - 使用
pytest
命令 - 同
nose2
的理念一樣,通過在配置文件中指定特定參數(shù)污呼,可配置用例文件裕坊、類和函數(shù)的名稱模式(模糊匹配)
pytest
也支持執(zhí)行指定用例:
- 指定測試文件路徑
pytest /path/to/test/file.py
- 指定測試類
pytest /path/to/test/file.py:TestCase
- 指定測試方法
pytest another.test::TestClass::test_method
- 指定測試函數(shù)
pytest /path/to/test/file.py:test_function
四、測試夾具(Fixtures)
pytest
的測試夾具和 unittest
曙求、nose
碍庵、nose2
的風格迥異,它不但能實現(xiàn) setUp
和 tearDown
這種測試前置和清理邏輯悟狱,還其他非常多強大的功能静浴。
4.1 聲明和使用
pytest
中的測試夾具更像是測試資源,你只需定義一個夾具挤渐,然后就可以在用例中直接使用它苹享。得益于 pytest
的依賴注入機制,你無需通過from xx import xx
的形式顯示導(dǎo)入,只需要在測試函數(shù)的參數(shù)中指定同名參數(shù)即可得问,比如:
import pytest
@pytest.fixture
def smtp_connection():
import smtplib
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
上述示例中定義了一個測試夾具 smtp_connection
囤攀,在測試函數(shù) test_ehlo
簽名中定義了同名參數(shù),則 pytest
框架會自動注入該變量宫纬。
4.2 共享
在 pytest
中焚挠,同一個測試夾具可被多個測試文件中的多個測試用例共享。只需在包(Package)中定義 conftest.py
文件漓骚,并把測試夾具的定義寫在該文件中蝌衔,則該包內(nèi)所有模塊(Module)的所有測試用例均可使用 conftest.py
中所定義的測試夾具。
比如蝌蹂,如果在如下文件結(jié)構(gòu)的 test_1/conftest.py
定義了測試夾具噩斟,那么 test_a.py
和 test_b.py
可以使用該測試夾具;而 test_c.py
則無法使用孤个。
`-- test_1
| |-- conftest.py
| `-- test_a.py
| `-- test_b.py
`-- test_2
`-- test_c.py
4.3 生效級別
unittest
和 nose
均支持測試前置和清理的生效級別:測試方法剃允、測試類和測試模塊。
pytest
的測試夾具同樣支持各類生效級別齐鲤,且更加豐富斥废。通過在pytest.fixture 中指定 scope
參數(shù)來設(shè)置:
- function —— 函數(shù)級,即調(diào)用每個測試函數(shù)前佳遂,均會重新生成 fixture
- class —— 類級营袜,調(diào)用每個測試類前,均會重新生成 fixture
- module —— 模塊級丑罪,載入每個測試模塊前,均會重新生成 fixture
- package —— 包級凤壁,載入每個包前吩屹,均會重新生成 fixture
- session —— 會話級,運行所有用例前拧抖,只生成一次 fixture
當我們指定生效級別為模塊級時煤搜,示例如下:
import pytest
import smtplib
@pytest.fixture(scope="module")
def smtp_connection():
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
4.4 測試前置和清理
pytest
的測試夾具也能夠?qū)崿F(xiàn)測試前置和清理,通過 yield
語句來拆分這兩個邏輯唧席,寫法變得很簡單擦盾,如:
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection():
smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
yield smtp_connection # provide the fixture value
print("teardown smtp")
smtp_connection.close()
在上述示例中,yield smtp_connection
及前面的語句相當于測試前置淌哟,通過 yield
返回準備好的測試資源 smtp_connection
; 而后面的語句則會在用例執(zhí)行結(jié)束(確切的說是測試夾具的生效級別的聲明周期結(jié)束時)后執(zhí)行迹卢,相當于測試清理。
如果生成測試資源(如示例中的 smtp_connection
)的過程支持 with
語句徒仓,那么還可以寫成更加簡單的形式:
@pytest.fixture(scope="module")
def smtp_connection():
with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection:
yield smtp_connection # provide the fixture value
pytest
的測試夾具除了文中介紹到的這些功能腐碱,還有諸如參數(shù)化夾具、工廠夾具掉弛、在夾具中使用夾具等更多高階玩法症见。
五喂走、跳過測試和預(yù)計失敗
pytest
除了支持 unittest
和 nosetest
的跳過測試和預(yù)計失敗的方式外,還在 pytest.mark
中提供對應(yīng)方法:
- 通過 skip裝飾器或 pytest.skip函數(shù)直接跳過測試
- 通過 skipif按條件跳過測試
- 通過 xfail預(yù)計測試失敗
示例如下:
@pytest.mark.skip(reason="no way of currently testing this")
def test_mark_skip():
...
def test_skip():
if not valid_config():
pytest.skip("unsupported configuration")
@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher")
def test_mark_skip_if():
...
@pytest.mark.xfail
def test_mark_xfail():
...
六谋作、子測試/參數(shù)化測試
pytest
除了支持 unittest
中的 TestCase.subTest
芋肠,還支持一種更為靈活的子測試編寫方式,也就是 參數(shù)化測試
遵蚜,通過 pytest.mark.parametrize
裝飾器實現(xiàn)帖池。
在下面的示例中,定義一個 test_eval
測試函數(shù)谬晕,通過 pytest.mark.parametrize
裝飾器指定 3 組參數(shù)碘裕,則將生成 3 個子測試:
@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):
assert eval(test_input) == expected
示例中故意讓最后一組參數(shù)導(dǎo)致失敗,運行用例可以看到豐富的測試結(jié)果輸出:
========================================= test session starts =========================================
platform darwin -- Python 3.7.1, pytest-4.0.1, py-1.7.0, pluggy-0.8.0
rootdir: /Users/prodesire/projects/tests, inifile:
plugins: cov-2.6.0
collected 3 items
test.py ..F [100%]
============================================== FAILURES ===============================================
__________________________________________ test_eval[6*9-42] __________________________________________
test_input = '6*9', expected = 42
@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):
> assert eval(test_input) == expected
E AssertionError: assert 54 == 42
E + where 54 = eval('6*9')
test.py:6: AssertionError
================================= 1 failed, 2 passed in 0.09 seconds ==================================
若將參數(shù)換成 pytest.param
攒钳,我們還可以有更高階的玩法帮孔,比如知道最后一組參數(shù)是失敗的,所以將它標記為 xfail:
@pytest.mark.parametrize(
"test_input,expected",
[("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)],
)
def test_eval(test_input, expected):
assert eval(test_input) == expected
如果測試函數(shù)的多個參數(shù)的值希望互相排列組合不撑,我們可以這么寫:
@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_foo(x, y):
pass
上述示例中會分別把 x=0/y=2
文兢、x=1/y=2
、x=0/y=3
和x=1/y=3
帶入測試函數(shù)焕檬,視作四個測試用例來執(zhí)行姆坚。
七、測試結(jié)果輸出
pytest
的測試結(jié)果輸出相比于 unittest
和 nose
來說更為豐富实愚,其優(yōu)勢在于:
- 高亮輸出兼呵,通過或不通過會用不同的顏色進行區(qū)分
- 更豐富的上下文信息,自動輸出代碼上下文和變量信息
- 測試進度展示
- 測試結(jié)果輸出布局更加友好易讀
如果對軟件測試腊敲、接口測試击喂、自動化測試、面試經(jīng)驗交流碰辅。感興趣可以加軟件測試交流:1085991341懂昂,還會有同行一起技術(shù)交流。
八没宾、插件體系
pytest
的插件十分豐富凌彬,而且即插即用,作為使用者不需要編寫額外代碼循衰。
此外铲敛,得益于 pytest
良好的架構(gòu)設(shè)計和鉤子機制,其插件編寫也變得容易上手羹蚣。
九原探、總結(jié)
三篇關(guān)于 Python 測試框架的介紹到這里就要收尾了。寫了這么多,各位看官怕也是看得累了咽弦。我們不妨羅列一個橫向?qū)Ρ缺硗襟。瑏砜偨Y(jié)下這些單元測試框架的異同:
Python 的單元測試框架看似種類繁多管呵,實則是一代代的進化梳毙,有跡可循。抓住其特點捐下,結(jié)合使用場景账锹,就能容易的做出選擇。
若你不想安裝或不允許第三方庫坷襟,那么 unittest
是最好也是唯一的選擇牌废。反之,pytest
無疑是最佳選擇啤握,眾多 Python 開源項目都是使用 pytest
作為單元測試框架。
以上內(nèi)容就是本篇的全部內(nèi)容以上內(nèi)容希望對你有幫助晶框,有被幫助到的朋友歡迎點贊排抬,評論。