Python最火的第三方開源測試框架 ——pytest

一、介紹

本篇文章是介紹的是Python 世界中最火的第三方單元測試框架:pytest。
它有如下主要特性:

  • assert 斷言失敗時輸出詳細信息(再也不用去記憶 self.assert* 名稱了)
  • 自動發(fā)現(xiàn)測試模塊和函數(shù)
  • 模塊化夾具用以管理各類測試資源
  • unittest 完全兼容,對 nose 基本兼容
  • 非常豐富的插件體系,有超過 315 款第三方插件糯而,社區(qū)繁榮

和前面介紹 unittestnose 一樣,我們將從如下幾個方面介紹 pytest 的特性泊窘。

二熄驼、用例編寫

nose 一樣,pytest 支持函數(shù)烘豹、測試類形式的測試用例瓜贾。最大的不同點是,你可以盡情地使用 assert 語句進行斷言携悯,絲毫不用擔心它會在 noseunittest 中產(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 既輸出了測試代碼上下文,也輸出了被測變量值的信息轴或。相比于 noseunittest昌跌,pytest 允許用戶使用更簡單的方式編寫測試用例,又能得到一個更豐富和友好的測試結(jié)果照雁。

三蚕愤、用例發(fā)現(xiàn)和執(zhí)行

unittestnose 所支持的用例發(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) setUptearDown這種測試前置和清理邏輯悟狱,還其他非常多強大的功能静浴。

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.pytest_b.py 可以使用該測試夾具;而 test_c.py 則無法使用孤个。

`-- test_1
|   |-- conftest.py
|   `-- test_a.py
|   `-- test_b.py
`-- test_2
    `-- test_c.py

4.3 生效級別

unittestnose 均支持測試前置和清理的生效級別:測試方法剃允、測試類和測試模塊。

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 除了支持 unittestnosetest 的跳過測試和預(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=2x=0/y=3x=1/y=3帶入測試函數(shù)焕檬,視作四個測試用例來執(zhí)行姆坚。

七、測試結(jié)果輸出

pytest 的測試結(jié)果輸出相比于 unittestnose 來說更為豐富实愚,其優(yōu)勢在于:

  • 高亮輸出兼呵,通過或不通過會用不同的顏色進行區(qū)分
  • 更豐富的上下文信息,自動輸出代碼上下文和變量信息
  • 測試進度展示
  • 測試結(jié)果輸出布局更加友好易讀

如果對軟件測試腊敲、接口測試击喂、自動化測試、面試經(jīng)驗交流碰辅。感興趣可以加軟件測試交流:1085991341懂昂,還會有同行一起技術(shù)交流。

八没宾、插件體系

pytest 的插件十分豐富凌彬,而且即插即用,作為使用者不需要編寫額外代碼循衰。

此外铲敛,得益于 pytest 良好的架構(gòu)設(shè)計和鉤子機制,其插件編寫也變得容易上手羹蚣。

九原探、總結(jié)

三篇關(guān)于 Python 測試框架的介紹到這里就要收尾了。寫了這么多,各位看官怕也是看得累了咽弦。我們不妨羅列一個橫向?qū)Ρ缺硗襟。瑏砜偨Y(jié)下這些單元測試框架的異同:

unittest nose nose2 pytest
自動發(fā)現(xiàn)用例
指定(各級別)用例執(zhí)行
支持 assert 斷言
測試夾具
測試夾具種類 前置和清理 前置和清理 前置和清理 前置、清理型型、內(nèi)置各類 fixtures段审,自定義各類 fixtures
測試夾具生效級別 方法、類闹蒜、模塊 方法寺枉、類、模塊 方法绷落、類姥闪、模塊 方法、類砌烁、模塊筐喳、包、會話
支持跳過測試和預(yù)計失敗
子測試
測試結(jié)果輸出 一般 較好 較好
插件 - 較豐富 一般 豐富
鉤子 - -
社區(qū)生態(tài) 作為標準庫函喉,由官方維護 停止維護 維護中避归,活躍度低 維護中,活躍度高

Python 的單元測試框架看似種類繁多管呵,實則是一代代的進化梳毙,有跡可循。抓住其特點捐下,結(jié)合使用場景账锹,就能容易的做出選擇。

若你不想安裝或不允許第三方庫坷襟,那么 unittest 是最好也是唯一的選擇牌废。反之,pytest 無疑是最佳選擇啤握,眾多 Python 開源項目都是使用 pytest 作為單元測試框架。
以上內(nèi)容就是本篇的全部內(nèi)容以上內(nèi)容希望對你有幫助晶框,有被幫助到的朋友歡迎點贊排抬,評論。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末授段,一起剝皮案震驚了整個濱河市蹲蒲,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌侵贵,老刑警劉巖届搁,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡卡睦,警方通過查閱死者的電腦和手機宴胧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來表锻,“玉大人恕齐,你說我怎么就攤上這事∷惭罚” “怎么了显歧?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長确镊。 經(jīng)常有香客問我士骤,道長,這世上最難降的妖魔是什么蕾域? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任拷肌,我火速辦了婚禮,結(jié)果婚禮上束铭,老公的妹妹穿的比我還像新娘廓块。我一直安慰自己,他們只是感情好契沫,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布带猴。 她就那樣靜靜地躺著,像睡著了一般懈万。 火紅的嫁衣襯著肌膚如雪拴清。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天会通,我揣著相機與錄音口予,去河邊找鬼。 笑死涕侈,一個胖子當著我的面吹牛沪停,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播裳涛,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼木张,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了端三?” 一聲冷哼從身側(cè)響起舷礼,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎郊闯,沒想到半個月后妻献,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蛛株,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年育拨,在試婚紗的時候發(fā)現(xiàn)自己被綠了谨履。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡至朗,死狀恐怖屉符,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情锹引,我是刑警寧澤矗钟,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站嫌变,受9級特大地震影響吨艇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜腾啥,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一东涡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧倘待,春花似錦疮跑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至啊奄,卻和暖如春渐苏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背菇夸。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工琼富, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人庄新。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓鞠眉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親择诈。 傳聞我的和親對象是個殘疾皇子凡蚜,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353