開發與維運

pytest 自動化測試框架(二)

本文節選自霍格沃玆測試學院內部教材,文末鏈接進階學習。

上一篇文章中分享了 pytest 的基本用法,本文進一步介紹 pytest 的其他實用特性和進階技巧。

pytest fixtures

pytest 中可以使用 @pytest.fixture 裝飾器來裝飾一個方法,被裝飾方法的方法名可以作為一個參數傳入到測試方法中。可以使用這種方式來完成測試之前的初始化,也可以返回數據給測試函數。

將 fixture 作為函數參數

通常使用 setup 和 teardown 來進行資源的初始化。如果有這樣一個場景,測試用例 1 需要依賴登錄功能,測試用例 2 不需要登錄功能,測試用例 3 需要登錄功能。這種場景 setup,teardown 無法實現,可以使用 pytest fixture 功能,在方法前面加個 @pytest.fixture 裝飾器,加了這個裝飾器的方法可以以參數的形式傳入到方法裡面執行。

例如在登錄的方法,加上 @pytest.fixture 這個裝飾器後,將這個用例方法名以參數的形式傳到方法裡,這個方法就會先執行這個登錄方法,再去執行自身的用例步驟,如果沒有傳入這個登錄方法,就不執行登錄操作,直接執行已有的步驟。

創建一個文件名為“test_fixture.py”,代碼如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pytest

@pytest.fixture()
def login():
    print("這是個登錄方法")
    return ('tom','123')

@pytest.fixture()
def operate():
    print("登錄後的操作")

def test_case1(login,operate):
    print(login)
    print("test_case1,需要登錄")

def test_case2():
    print("test_case2,不需要登錄 ")

def test_case3(login):
    print(login)
    print("test_case3,需要登錄")

在上面的代碼中,測試用例 test_case1 和 test_case3 分別增加了 login 方法名作為參數,pytest 會發現並調用 @pytest.fixture 標記的 login 功能,運行測試結果如下:

plugins: html-2.0.1, rerunfailures-8.0, xdist-1.31.0, \
ordering-0.6, forked-1.1.3, allure-pytest-2.8.11, metadata-1.8.0
collecting ... collected 3 items

test_fixture.py::test_case1 這是個登錄方法
登錄後的操作
PASSED     [ 33%]('tom', '123')
test_case1,需要登錄

test_fixture.py::test_case2 PASSED \
[ 66%]test_case2,不需要登錄 

test_fixture.py::test_case3 這是個登錄方法
PASSED      [100%]('tom', '123')
test_case3,需要登錄

============================== 3 passed in 0.02s ===============================

Process finished with exit code 0

從上面的結果可以看出,test_case1 和 test_case3 運行之前執行了 login 方法,test_case2 沒有執行這個方法。

指定範圍內共享

fixture 裡面有一個參數 scope,通過 scope 可以控制 fixture 的作用範圍,根據作用範圍大小劃分:session> module> class> function,具體作用範圍如下:

  • function 函數或者方法級別都會被調用
  • class 類級別調用一次
  • module 模塊級別調用一次
  • session 是多個文件調用一次(可以跨.py文件調用,每個.py文件就是module)
    例如整個模塊有多條測試用例,需要在全部用例執行之前打開瀏覽器,全部執行完之後去關閉瀏覽器,打開和關閉操作只執行一次,如果每次都重新執行打開操作,會非常佔用系統資源。這種場景除了setup_module,teardown_module 可以實現,還可以通過設置模塊級別的 fixture 裝飾器(@pytest.fixture(scope="module"))來實現。

scope='module'

fixture 參數 scope='module',module 作用是整個模塊都會生效。

創建文件名為 test_fixture_scope.py,代碼如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pytest

# 作用域:module是在模塊之前執行, 模塊之後執行
@pytest.fixture(scope="module")
def open():
    print("打開瀏覽器")
    yield

    print("執行teardown !")
    print("最後關閉瀏覽器")

@pytest.mark.usefixtures("open")
def test_search1():
    print("test_search1")
    raise NameError
    pass

def test_search2(open):
    print("test_search2")
    pass

def test_search3(open):
    print("test_search3")
    pass

代碼解析:

@pytest.fixture() 如果不寫參數,參數默認 scope='function'。當 scope='module' 時,在當前 .py 腳本里面所有的用例開始前只執行一次。scope 巧妙與 yield 組合使用,相當於 setup 和 teardown 方法。還可以使用 @pytest.mark.usefixtures 裝飾器,傳入前置函數名作為參數。

運行結果如下:

plugins: html-2.0.1, rerunfailures-8.0, \
xdist-1.31.0, ordering-0.6, forked-1.1.3,\
 allure-pytest-2.8.11, metadata-1.8.0
collecting ... collected 3 items

test_fixture_yield.py::test_search1 打開瀏覽器
FAILED  [ 33%]test_search1

test_fixture_yield.py:13 (test_search1)
open = None

    def test_search1(open):
        print("test_search1")
>       raise NameError
E       NameError

test_fixture_yield.py:16: NameError

test_fixture_yield.py::test_search2 PASSED  \
[ 66%]test_search2

test_fixture_yield.py::test_search3 PASSED   \
[100%]test_search3
執行teardown !
最後關閉瀏覽器
...

open = None

    def test_search1(open):
        print("test_search1")
>       raise NameError
E       NameError

test_fixture_yield.py:16: NameError
------ Captured stdout setup --------
打開瀏覽器
----- Captured stdout call -----
test_search1
===== 1 failed, 2 passed in 0.06s =====

Process finished with exit code 0

從上面運行結果可以看出,scope="module" 與 yield 結合,相當於 setup_module 和 teardown_module 方法。整個模塊運行之前調用了 open()方法中 yield 前面的打印輸出“打開瀏覽器”,整個運行之後調用了 yield 後面的打印語句“執行 teardown !”與“關閉瀏覽器”。yield 來喚醒 teardown 的執行,如果用例出現異常,不影響 yield 後面的 teardown 執行。可以使用 @pytest.mark.usefixtures 裝飾器來進行方法的傳入。

conftest.py 文件

fixture scope 為 session 級別是可以跨 .py 模塊調用的,也就是當我們有多個 .py 文件的用例時,如果多個用例只需調用一次 fixture,可以將 scope='session',並且寫到 conftest.py 文件裡。寫到 conftest.py 文件可以全局調用這裡面的方法。使用的時候不需要導入 conftest.py 這個文件。使用 conftest.py 的規則:

conftest.py 這個文件名是固定的,不可以更改。
conftest.py 與運行用例在同一個包下,並且該包中有 init.py 文件
使用的時候不需要導入 conftest.py,pytest 會自動識別到這個文件
放到項目的根目錄下可以全局調用,放到某個 package 下,就在這個 package 內有效。

案例

在運行整個項目下的所有的用例,只執行一次打開瀏覽器。執行完所有的用例之後再執行關閉瀏覽器,可以在這個項目下創建一個 conftest.py 文件,將打開瀏覽器操作的方法放在這個文件下,並添加一個裝飾器 @pytest.fixture(scope="session"),就能夠實現整個項目所有測試用例的瀏覽器複用,案例目錄結構如下:
image.png

創建目錄 test_scope,並在目錄下創建三個文件 conftest.py,test_scope1.py 和 test_scope2.py。

conftest.py 文件定義了公共方法,pytest 會自動讀取 conftest.py 定義的方法,代碼如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pytest

@pytest.fixture(scope="session")
def open():
    print("打開瀏覽器")
    yield

    print("執行teardown !")
    print("最後關閉瀏覽器")

創建 test_scope1.py 文件,代碼如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pytest

def test_search1(open):
    print("test_search1")
    pass

def test_search2(open):
    print("test_search2")
    pass

def test_search3(open):
    print("test_search3")
    pass

if __name__ == '__main__':
    pytest.main()

創建文件“test_scope2.py”,代碼如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

class TestFunc():
    def test_case1(self):
        print("test_case1,需要登錄")

    def test_case2(self):
        print("test_case2,不需要登錄 ")

    def test_case3(self):
        print("test_case3,需要登錄")

打開 cmd,進入目錄 test_scope/,執行如下命令:

pytest -v -s  

或者

pytest -v -s test_scope1.py test_scope2.py

執行結果如下:

省略...
collected 6 items                                                                                                   
test_scope1.py::test_search1 打開瀏覽器
test_search1
PASSED
test_scope1.py::test_search2 test_search2
PASSED
test_scope1.py::test_search3 test_search3
PASSED
test_scope2.py::TestFunc::test_case1 test_case1,需要登錄
PASSED
test_scope2.py::TestFunc::test_case2 test_case2,不需要登錄 
PASSED
test_scope2.py::TestFunc::test_case3 test_case3,需要登錄
PASSED執行teardown !
最後關閉瀏覽器

省略後面打印結果...

執行過程中 pytest 會自動識別當前目錄的 conftest.py,不需要導入直接引用裡面的方法配置。應用到整個目錄下的所有調用這裡面的方法中執行。conftest.py 與運行的用例要在同一個 pakage 下,並且這個包下有 __init__.py 文件

自動執行 fixture

如果每條測試用例都需要添加 fixture 功能,則需要在每一要用例方法裡面傳入這個fixture的名字,這裡就可以在裝飾器裡面添加一個參數 autouse='true',它會自動應用到所有的測試方法中,只是這裡沒有辦法返回值給測試用例。

使用方法,在方法前面加上裝飾器,如下:

@pytest.fixture(autouse="true")
def myfixture():
    print("this is my fixture")

@pytest.fixture 裡設置 autouse 參數值為 true(默認 false),每個測試函數都會自動調用這個前置函數。

創建文件名為“test_autouse.py”,代碼如下:

# coding=utf-8
import pytest

@pytest.fixture(autouse="true")
def myfixture():
    print("this is my fixture")


class TestAutoUse:
    def test_one(self):
        print("執行test_one")
        assert 1 + 2 == 3

    def test_two(self):
        print("執行test_two")
        assert 1 == 1

    def test_three(self):
        print("執行test_three")
        assert 1 + 1 == 2

執行上面這個測試文件,結果如下:

...
test_a.py::TestAutoUse::test_one this is my fixture
執行test_one
PASSED
test_a.py::TestAutoUse::test_two this is my fixture
執行test_two
PASSED
test_a.py::TestAutoUse::test_three this is my fixture
執行test_three
PASSED
...

從上面的運行結果可以看出,在方法 myfixture() 上面添加了裝飾器 @pytest.fixture(autouse="true"),測試用例無須傳入這個 fixture 的名字,它會自動在每條用例之前執行這個 fixture。

fixture 傳遞參數

測試過程中需要大量的測試數據,如果每條測試數據都編寫一條測試用例,用例數量將是非常寵大的。一般我們在測試過程中會將測試用到的數據以參數的形式傳入到測試用例中,併為每條測試數據生成一個測試結果數據。

這時候可以使用 fixture 的參數化功能,在 fixture 方法加上裝飾器 @pytest.fixture(params=[1,2,3]),就會傳入三個數據 1、2、3,分別將這三個數據傳入到用例當中。這裡可以傳入的數據是個列表。傳入的數據需要使用一個固定的參數名 request 來接收。

創建文件名為“test_params.py”,代碼如下:

import pytest

@pytest.fixture(params=[1, 2, 3])
def data(request):
    return request.param

def test_not_2(data):
    print(f"測試數據:{data}")
    assert data < 5

運行結果如下:

...

test_params.py::test_not_2[1]PASSED  [ 33%]測試數據:1

test_params.py::test_not_2[2] PASSED [ 66%]測試數據:2

test_params.py::test_not_2[3] PASSED [100%]測試數據:3

...

從運行結果可以看出,對於 params 裡面的每個值,fixture 都會去調用執行一次,使用 request.param 來接受用例參數化的數據,並且為每一個測試數據生成一個測試結果。在測試工作中使用這種參數化的方式,會減少大量的代碼量,並且便於閱讀與維護。

多線程並行與分佈式執行

假如項目中有測試用例 1000 條,一條測試用例需要執行 1 分鐘,一個測試人員需要 1000 分鐘才能完成一輪迴歸測試。通常我們會用人力成本換取時間成本,加幾個人一起執行,時間就會縮短。如果 10 人一起執行只需要 100 分鐘,這就是一種並行測試,分佈式的場景。

pytest-xdist 是 pytest 分佈式執行插件,可以多個 CPU 或主機執行,這款插件允許用戶將測試併發執行(進程級併發),插件是動態決定測試用例執行順序的,為了保證各個測試能在各個獨立線程里正確的執行,應該保證測試用例的獨立性(這也符合測試用例設計的最佳實踐)。

安裝

pip install pytest-xdist

多個 CPU 並行執行用例,需要在 pytest 後面添加 -n 參數,如果參數為 auto,會自動檢測系統的 CPU 數目。如果參數為數字,則指定運行測試的處理器進程數。

pytest -n auto   
pytest -n [num]  

案例

某個項目有 200 條測試用例,每條測試用例之間沒有關聯關係,互不影響。這 200 條測試用例需要在 1 小時之內測試完成,可以加個-n參數,使用多 CPU 並行測試。運行方法:

pytest -n 4

進入到項目目錄下,執行 pytest 可以將項目目錄下所有測試用例識別出來並且運行,加上 -n 參數,可以指定 4 個 CPU 併發執行。大量的測試用例併發執行提速非常明顯。

結合 pytest-html 生成測試報告

測試報告通常在項目中尤為重要,報告可以體現測試人員的工作量,開發人員可以從測試報告中瞭解缺陷的情況,因此測試報告在測試過程中的地位至關重要,測試報告為糾正軟件存在的質量問題提供依據,為軟件驗收和交付打下基礎。測試報告根據內容的側重點,可以分為 “版本測試報告” 和 “總結測試報告”。執行完 pytest 測試用例,可以使用 pytest-HTML 插件生成 HTML 格式的測試報告。

安裝

pip install pytest-html

執行方法

pytest --html=path/to/html/report.html

結合 pytest-xdist 使用

pytest -v -s -n 3 --html=report.html --self-contained-html 

生成測試報告

如下圖:
image.png

生成的測試報告最終是 HTML 格式,報告內容包括標題、運行時間、環境、彙總結果以及用例的通過個數、跳過個數、失敗個數、錯誤個數,期望失敗個數、不期望通過個數、重新運行個數、以及錯誤的詳細展示信息。報告會生成在運行腳本的同一路徑,需要指定路徑添加--html=path/to/html/report.html 這個參數配置報告的路徑。如果不添加 --self-contained-html 這個參數,生成報告的 CSS 文件是獨立的,分享的時候容易千萬數據丟失。

pytest 框架 assert 斷言使用(附)

編寫代碼時,我們經常會做出一些假設,斷言就是用於在代碼中捕捉這些假設。斷言表示為一些布爾表達式,測試人員通常會加一些斷言來斷定中間過程的正確性。斷言支持顯示最常見的子表達式的值,包括調用,屬性,比較以及二元和一元運算符。Python使用 assert(斷言)用於判斷一個表達式,在表達式條件為 false 的時候觸發異常。

使用方法:

assert True         #斷言為真
assertnot False     #斷言為假

案例如下:

assert "h" in "hello"  #判斷h在hello中
assert 5>6             #判斷5>6為真    
assert not True        #判斷xx不為真
assert {'0', '1', '3', '8'} == {'0', '3', '5', '8'}     #判斷兩個字典相等

如果沒有斷言,沒有辦法判定用例中每一個測試步驟結果的正確性。在項目中適當的使用斷言,來對代碼的結構、屬性、功能、安全性等場景檢查與驗證。

更多技術文章分享及測試資料點此獲取

Leave a Reply

Your email address will not be published. Required fields are marked *