天天看點

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"),就能夠實作整個項目所有測試用例的浏覽器複用,案例目錄結構如下:

pytest 自動化測試架構(二)

建立目錄 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            

生成測試報告

如下圖:

pytest 自動化測試架構(二)

生成的測試報告最終是 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'}     #判斷兩個字典相等           

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

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