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 創建測試用例

  1. 創建test_開頭的python文件
  2. 編寫test_開頭的函數
  3. 在函數中使用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

測試發現:搜集用例

一般規則:

  1. 從當前目錄開始,遍歷每一個子目錄 (不論這個目錄是不是包)
  2. 在目錄搜索test_*.py*_test.py,並導入(測試文件中的代碼會自動執行)
  3. 在導入的模塊手機以下特征的對象,作為測試用例
    1. test開頭的函數
    2. Test開頭類及其test開頭方法 (這個類不應該有__init__
    3. 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報告分為幾個基本部分:

  1. 報告頭
  2. 用例收集情況
  3. 執行狀態
    1. 用例的結果
    2. 進度
  4. 信息
    1. 錯誤信息
    2. 統計信息
    3. 耗時信息

報告中的結果縮寫符合是什么含義

符號 含義
. 測試通過
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處理方式不一樣:

  1. 數值比較:會顯示具體數值

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. 夾具

單元代碼?

創建測試用例:

  1. 創建test_開頭的函數
  2. 在函數內使用斷言關鍵字

一個測試用例的執行分為四個步驟:

  1. 預置條件
  2. 執行動作
  3. 斷言結果
  4. 清理現場

為了重復測試結果不會異常,也為了不會干擾其他用例。

在理想情況,為了突出用例重點,用例中應該只有2(執行動作)和3(斷言結果)

  • 1 和4 應當封裝起來
  • 1 和4 能夠自動執行

夾具(Fixture)是軟件測試裝置,作用和目的:

  • 在測試開始前,准備好相關的測試環境
  • 在測試結束后,銷毀相關的內容

以socket聊天服務器作為例子,演示夾具的用法

socket服務的測試步驟:

  1. 建立socket連接
  2. 利用socket執行測試動作
  3. 對結果進行斷言
  4. 斷開socket

3.1 創建夾具

3.1.1 快速上手

夾具的特性:

  1. 在測試用例之前執行
  2. 具體重復性和必要性

夾具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個部分:

  1. yield之前的代碼,在測試前執行,對應xUnit中setUP
  2. yield 之后的代碼,在測試后執行,對應xUnit中yeadDown

3.1.3 夾具范圍

夾具生命周期:

  1. 被需要用的時候創建
  2. 在結束范圍的時候銷毀
  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執行邏輯:

  1. 運行所有的測試用例
  2. 執行用例的時候,出現異常,判斷為測試失敗
  3. 執行用例的時候,沒有出現異常,判斷為測試通過

標記是給測試用例用的

標記的作用,就是為了改變默認行為:

  • 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,1 => 2, 1,0=>1, 9999999999,1=>100000000
  2. 反復測試,驗證測試結果穩定性 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 配置方法

  1. 命令行
    • 靈活
    • 如果有多個選項的話,不方便
  2. 配置文件
    • 特別適合大量,或者不常修改的選項
    • pytest.ini
    • pyproject.toml
      • pytest 6.0+ 支持
      • 是PEP標准
      • 是未來
  3. 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 配置項

  1. 查詢幫助信息 pytest -h
  2. 查看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. 布局

特性:

  1. 如果一個測試文件,存放在目錄中,那么執行時,這個目錄成為頂級目錄
  2. 如果一個測試文件,存放在包中,那么執行時,根目錄成為頂級目錄
  3. python -m pytest ,將當前目錄加入到sys.path ,當前目錄中的模塊可以被導入


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM