天天看點

#yyds幹貨盤點# pytest測試架構詳解

大家好,我是玉米君,本篇文章将從基礎、斷言、夾具、标記、配置、插件和布局幾個方面帶大家了解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.yumi
def test_sanmu():
    pass


@pytest.mark.mmm
@pytest.mark.danny
def test_yiran():
    pass
           

注冊自定義标記:pytest知道哪些自定義标記是正确的,就不會發出警告

# pytest.ini
[pytest]
markers =
 mmm
 yumi
 danny
           

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", "yumi")
    config.addinivalue_line("markers", "danny")
           

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

    ,目前目錄中的子產品可以被導入