多是 Python 中最火的第三方開源測試框架 pytest


做者:HelloGitHub-Prodesirehtml


文中涉及的示例代碼,已同步更新到python

HelloGitHub-Team 倉庫
git

1、介紹

本篇文章是《聊聊 Python 的單元測試框架》的第三篇,前兩篇分別介紹了標準庫 unittest 和第三方單元測試框架 nose。做爲本系列的最後一篇,壓軸出場的是Python 世界中最火的第三方單元測試框架:pytest。github

pytest 項目地址:github.com/pytest-dev/…編程

它有以下主要特性:bash

和前面介紹 unittestnose 同樣,咱們將從以下幾個方面介紹 pytest 的特性。session

2、用例編寫

nose 同樣,pytest 支持函數、測試類形式的測試用例。最大的不一樣點是,你能夠盡情地使用 assert 語句進行斷言,絲絕不用擔憂它會在 noseunittest 中產生的缺失詳細上下文信息的問題。架構

好比下面的測試示例中,故意使得 test_upper 中斷言不經過:app

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 去執行用例時,它會輸出詳細的(且是多種顏色)上下文信息:框架

=================================== 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 既輸出了測試代碼上下文,也輸出了被測變量值的信息。相比於 noseunittestpytest 容許用戶使用更簡單的方式編寫測試用例,又能獲得一個更豐富和友好的測試結果。

3、用例發現和執行

unittestnose 所支持的用例發現和執行能力,pytest 均支持。 pytest 支持用例自動(遞歸)發現:

  • 默認發現當前目錄下全部符合 test_*.py*_test.py 的測試用例文件中,以 test 開頭的測試函數或以 Test 開頭的測試類中的以 test 開頭的測試方法
    • 使用 pytest 命令
  • nose2 的理念同樣,經過在配置文件中指定特定參數,可配置用例文件、類和函數的名稱模式(模糊匹配)

pytest 也支持執行指定用例:

  • 指定測試文件路徑
    • pytest /path/to/test/file.py
  • 指定測試類
    • pytest /path/to/test/file.py:TestCase
  • 指定測試方法
    • pytest another.test::TestClass::test_method
  • 指定測試函數
    • pytest /path/to/test/file.py:test_function

4、測試夾具(Fixtures)

pytest測試夾具unittestnosenose2的風格迥異,它不但能實現 setUptearDown這種測試前置和清理邏輯,還其餘很是多強大的功能。

4.1 聲明和使用

pytest 中的測試夾具更像是測試資源,你只需定義一個夾具,而後就能夠在用例中直接使用它。得益於 pytest 的依賴注入機制,你無需經過from xx import xx的形式顯示導入,只須要在測試函數的參數中指定同名參數便可,好比:

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,在測試函數 test_ehlo 簽名中定義了同名參數,則 pytest 框架會自動注入該變量。

4.2 共享

pytest 中,同一個測試夾具可被多個測試文件中的多個測試用例共享。只需在包(Package)中定義 conftest.py 文件,並把測試夾具的定義寫在該文件中,則該包內全部模塊(Module)的全部測試用例都可使用 conftest.py 中所定義的測試夾具。

好比,若是在以下文件結構的 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 參數來設置:

  • function —— 函數級,即調用每一個測試函數前,均會從新生成 fixture
  • class —— 類級,調用每一個測試類前,均會從新生成 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 的測試夾具也可以實現測試前置和清理,經過 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; 然後面的語句則會在用例執行結束(確切的說是測試夾具的生效級別的聲明週期結束時)後執行,至關於測試清理。

若是生成測試資源(如示例中的 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 的測試夾具除了文中介紹到的這些功能,還有諸如參數化夾具工廠夾具在夾具中使用夾具等更多高階玩法,詳情請閱讀 "pytest fixtures: explicit, modular, scalable"

5、跳過測試和預計失敗

pytest 除了支持 unittestnosetest 的跳過測試和預計失敗的方式外,還在 pytest.mark 中提供對應方法:

  • 經過 skip 裝飾器或 pytest.skip 函數直接跳過測試
  • 經過 skipif按條件跳過測試
  • 經過 xfail 預計測試失敗

示例以下:

@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():
    ...複製代碼

關於跳過測試和預計失敗的更多玩法,參見 "Skip and xfail: dealing with tests that cannot succeed"

6、子測試/參數化測試

pytest 除了支持 unittest 中的 TestCase.subTest,還支持一種更爲靈活的子測試編寫方式,也就是 參數化測試,經過 pytest.mark.parametrize 裝飾器實現。

在下面的示例中,定義一個 test_eval 測試函數,經過 pytest.mark.parametrize 裝飾器指定 3 組參數,則將生成 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複製代碼

示例中故意讓最後一組參數致使失敗,運行用例能夠看到豐富的測試結果輸出:

========================================= 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 ==================================複製代碼

若將參數換成 pytest.param,咱們還能夠有更高階的玩法,好比知道最後一組參數是失敗的,因此將它標記爲 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複製代碼

若是測試函數的多個參數的值但願互相排列組合,咱們能夠這麼寫:

@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_foo(x, y):
    pass複製代碼

上述示例中會分別把 x=0/y=2x=1/y=2x=0/y=3x=1/y=3帶入測試函數,視做四個測試用例來執行。

7、測試結果輸出

pytest 的測試結果輸出相比於 unittestnose 來講更爲豐富,其優點在於:

  • 高亮輸出,經過或不經過會用不一樣的顏色進行區分
  • 更豐富的上下文信息,自動輸出代碼上下文和變量信息
  • 測試進度展現
  • 測試結果輸出佈局更加友好易讀

8、插件體系

pytest插件十分豐富,並且即插即用,做爲使用者不須要編寫額外代碼。關於插件的使用,參見"Installing and Using plugins"

此外,得益於 pytest 良好的架構設計和鉤子機制,其插件編寫也變得容易上手。關於插件的編寫,參見"Writing plugins"

9、總結

三篇關於 Python 測試框架的介紹到這裏就要收尾了。寫了這麼多,各位看官怕也是看得累了。咱們不妨羅列一個橫向對比表,來總結下這些單元測試框架的異同:

unittest nose nose2 pytest
自動發現用例
指定(各級別)用例執行
支持 assert 斷言
測試夾具
測試夾具種類 前置和清理 前置和清理 前置和清理 前置、清理、內置各種 fixtures,自定義各種 fixtures
測試夾具生效級別 方法、類、模塊 方法、類、模塊 方法、類、模塊 方法、類、模塊、包、會話
支持跳過測試和預計失敗
子測試
測試結果輸出 通常 較好 較好
插件 - 較豐富 通常 豐富
鉤子 - -
社區生態 做爲標準庫,由官方維護 中止維護 維護中,活躍度低 維護中,活躍度高

Python 的單元測試框架看似種類繁多,實則是一代代的進化,有跡可循。抓住其特色,結合使用場景,就能容易的作出選擇。

若你不想安裝或不容許第三方庫,那麼 unittest 是最好也是惟一的選擇。反之,pytest 無疑是最佳選擇,衆多 Python 開源項目(如大名鼎鼎的 requests)都是使用 pytest 做爲單元測試框架。甚至,連 nose2官方文檔上都建議你們使用 pytest,這得是多大的敬佩呀!

『講解開源項目系列』
——讓對開源項目感興趣的人再也不畏懼、讓開源項目的發起者再也不孤單。跟着咱們的文章,你會發現編程的樂趣、使用和發現參與開源項目如此簡單。歡迎留言聯繫咱們、加入咱們,讓更多人愛上開源、貢獻開源~
相關文章
相關標籤/搜索