pytest的資料驅動和引數傳遞

向闲而过發表於2024-06-10

4.1引數化介紹

常見使用場景:簡單註冊功能,也就是輸入使用者名稱、輸入密碼、單擊註冊,而測試資料會有很多個,可以透過測試用例設計技術組織出很多測試資料,例如使用者名稱都是字母,密碼也都是字母,或者都是數字,也可是它們的組合,或是邊界值長度的測試資料等。

這時可以透過引數化技術實現測試資料驅動執行每組測試用例。測試資料與測試用例是多對一的關係,所以完全可以把它們分開來看,把資料部分抽象成引數,透過對引數的賦值來驅動用例的執行。引數化傳遞是實現資料驅動的一種技術,可以實現測試資料與測試用例分離。

各個方面的引數化如下:

  • 測試用例的引數化:使用@pytest.mark.parametrize可以在測試用例、測試類甚至測試模組中標記多個引數或fixture的組合;
  • 引數化的行為可以表現在不同的層級上;
  • 多引數的引數化:一個以上引數與資料驅動結果;
  • 自定義引數化:可以透過pytest_generate_tests這個鉤子方法自定義引數化的方案;
  • 使用第三方外掛實現資料驅動DDT。

4.2引數化的應用

透過@pytest.mark.parametrize可以實現資料驅動。@pytest.mark.parametrize的根本作用是在收集測試用例的過程中,透過對指定引數的賦值來新增被標記物件的呼叫(執行)。下面以例說明具體引數化時如何使用不同資料。

4.2.1 單一引數化應用

通常使用場景:測試方法中只有一個資料是變化的,也就是透過一個引數把多組測試資料傳遞進去。執行時,每組資料都執行一遍。實現的具體步驟如下:

(1)在測試方法中輸入@pytest.mark.parametrize。

(2)其中有兩個引數,一個是引數名,另一個是引數值,這個值可以是多個,可以是數字或字元等。

(3)測試方法中的引數與parametrize中的引數名相同。

(4)透過引數名在測試方法中呼叫這些資料。

程式碼如下:

import pytest


@pytest.mark.parametrize("test_case", [1, 2, 3, "one", "two"])
def test_string(test_case):
    print(f"\n 測試資料{test_case}")

一個測試用例,有多少條資料就自動執行多少遍。執行的結果如下:

pytest的資料驅動和引數傳遞

4.2.2 多引數應用

測試輸入的資料可以是表示式,輸入的引數可以是多個。多個資料可以透過元組方式組織。下面是一個測試計算器的簡單例子,前面兩個是變數,後面是對應的資料。3+5對應的是test_input引數名,8對應的是expected引數名,下面的資料以此類推。eval將字串str

當成有效表示式來求值並返回計算結果。

程式碼如下:

import pytest


@pytest.mark.parametrize("test_input, expected", [("3+5", 8),("2+5", 7),("9+5", 80)])
def test_eval(test_input,expected):
    assert eval(test_input) == expected

將其中一組資料寫成錯誤的形式,驗證斷言的詳細情況。執行結果如下:

pytest的資料驅動和引數傳遞

4.2.3 多個引數化

一個用例可以標記多個@pytest.mark.parametrize標記。

程式碼如下:

import pytest


@pytest.mark.parametrize("test_input", [1, 2, 3])
@ pytest.mark.parametrize("test_output, expected", [(1, 2), (3, 4)])
def test_multi(test_input, test_output, expected):
    print(f"\n 測試結果{test_input}--{test_output}--{expected}")

實際收集到的用例是它們所有可能的組合。

pytest的資料驅動和引數傳遞

4.2.4 引數化與fixture的結合

當一個測試方法既是注入依賴,也就是使用fixture,同時又要引數化時,使用parametrize會有衝突,此時可以透過fixture自帶的引數params實現引數化。這也是引數化的一種方法。

4.2.5 pytestmark實現引數化

可以嘗試透過對pytestmark賦值,引數化一個測試模組。

程式碼如下:

import pytest

pytestmark = pytest.mark.parametrize("test_input, expected", [(1, 2), (3, 4)])


def test_module(test_input, expected):
    assert test_input + 1 == expected

結果如下:

pytest的資料驅動和引數傳遞

4.3parametrize原始碼詳細講解

下面透過兩個例子講解引數化技術。我們先來看一下它在原始碼中的定義。此方法在structures.py檔案中。

原始碼及部分翻譯如下:

pytest的資料驅動和引數傳遞

4.4argnames引數

183parametrize方法中的第一個引數argnames是一個用逗號分隔的字串,或者一個列表/元組,表明指定的引數名。argnames通常是與被標記測試方法入參的引數名對應的,但實際上有一些限制,它只能是被標記測試方法入參的子集。

4.4.1 argnames與測試方法中的引數關係

1.測試方法未宣告,mark.parametrize中宣告

test_sample1中並沒有宣告expected引數,如果在標記中強行宣告,則會得到如下錯誤。

程式碼如下:

import pytest


@pytest.mark.parametrize("input, expected", [(1, 2)])
def test_samplel(input):
    assert input + 1 == 1

執行的結果會提示下面所示的錯誤資訊:

In test_samplel: function uses no argument 'expected'

2.測試方法引數宣告的範圍小於mark.parametrize中宣告的範圍

不能是被標記測試方法入參中定義了預設值的引數。

程式碼如下:

import pytest


@pytest.mark.parametrize("input, expected", [(1, 2)])
def test_samplel2(input, expected=2):
    assert input + 1 == expected

雖然test_sample2宣告瞭expected引數,但同時也為其賦予了一個預設值,如果非要在標記中強行宣告,則會得到如下錯誤:

In test_samplel2: function already takes an argument 'expected' with a default value

4.4.2 argnames呼叫覆蓋同名的fixture

通常在使用fixture和引數parametrize時,可以一個引數使用引數化,另一個引數使用fixture和引數化,而同時使用fixture和引數化時,引數化的引數值會覆蓋原來fixture返回的值。

程式碼如下

import pytest


@pytest.fixture()
def expected():
    return 2


@pytest.fixture()
def input():
    return 0


@pytest.mark.parametrize("input", [(1)])
def test_sanple(input, expected):
    assert input + 1 == expected

執行結果:

pytest的資料驅動和引數傳遞

可以看到expected引數未使用引數化傳入資料,而是直接呼叫fixture中的返回值2,input同時使用引數化和fixture,引數化中引數值1覆蓋了原來fixture的返回值0,因此執行結果斷言應該是成功的。

引數化的引數可以不是fixture的,因此可以透過引數值傳入。

程式碼如下:

@pytest.fixture()
def expected():
    return 1


@pytest.mark.parametrize("input,expected", [(1, 2)])
def test_sanple(input,expected):
    assert input + 1 == expected

test_sample標記的input引數的值是由後面的(1,2)傳入的,expected引數(引數值為2)覆蓋了同名的fixture expected(返回值1),所以這條用例是可以測試成功的。

4.5argvalues引數

引數化中引數值argvalues是一個可迭代物件,表明對argnames引數的賦值,具體有以下幾種情況:如果argnames包含多個引數,那麼argvalues的迭代返回元素必須是可度量的值,即支援len()方法,並且長度和argnames所宣告引數的個數相等,所以它可以是元組/列表/集合等,表明所有入參的實參。

程式碼如下:

import pytest



@pytest.mark.parametrize("input,expected", [(1, 2), {2, 3}, set([3, 4])])
def test_sanple4(input, expected):
    print(expected)
    assert input + 1 == expected

執行結果如下:

pytest的資料驅動和引數傳遞

4.5.1 argvalues來源於Excel檔案

argvalues是一個可迭代物件,所以可以應用在更復雜的場景中,這在實際應用中被特別廣泛使用。公司一般會將測試資料儲存在Excel表中,或csv檔案中,或資料庫中。可以先

將資料讀取到列表中,這樣便可以在引數化的引數值中直接呼叫。例如:從Excel檔案中讀取實參。

程式碼如下:

import pytest


def read_excel():
    # 從資料庫或者excel中讀取資料資訊,這裡簡化成一個列表
    for dev in ["dev1", "dev2", "dev3"]:
        yield dev


@pytest.mark.parametrize("dev", read_excel())
def test_sample5(dev):
    print(dev)

執行結果如下:

pytest的資料驅動和引數傳遞

4.5.2 使用pytest.param為argvalues賦值

在結合pytest.param方法對skip和xfail標記中,可以使用pytest.param為argvalues引數賦值,讓執行有更詳細說明。

程式碼如下:

import pytest


@pytest.mark.parametrize(("n", "expected"), [(4, 2), pytest.param(6,3,marks=pytest.mark.xfail(), id="XPASS")])
def test_param(n,expected ):
    assert  n/2 == expected

執行結果:

pytest的資料驅動和引數傳遞

無論argvalues中傳遞的是可度量物件(列表、元組等)還是具體的值,在原始碼中都會將其封裝成一個ParameterSet物件,它是一個具名元組(namedtuple),包含values、marks、id 3個元素,程式碼如下:

pytest的資料驅動和引數傳遞

如果直接傳遞一個ParameterSet物件會發生什麼呢?原始碼如下:

pytest的資料驅動和引數傳遞

可以看到,如果直接傳遞一個ParameterSet物件,那麼返回的就是它本身(returnparameterset),所以下面例子中的兩種寫法是等價的。

pytest的資料驅動和引數傳遞

pytest.param的作用就是封裝一個ParameterSet物件。原始碼如下:

pytest的資料驅動和引數傳遞

4.6indirect引數

indirect是argnames的子集或者一個布林值。將指定引數的實參透過request.param重定向到和引數同名的fixture中,以此滿足更復雜的場景。預設indirect為False,使用mark.parametrize後的資料。當indirect為True時,使用fixture中的資料。

程式碼如下:

import pytest


@pytest.fixture()
def max(request):
    print("max", request.param)
    return request.param - 1


@pytest.fixture()
def min(request):
    return request.param + 1


# 預設indirect為False,min和max使用的後面的資料,
@pytest.mark.parametrize("min, max", [(1, 2), (3, 4)])
def test_indirect(min, max):
    assert min <= max


# min和max對於的實參重定向重名的fixture中,min和max使用的是fixture的資料
@pytest.mark.parametrize("min, max", [(1, 2), (3, 4)], indirect=True)
def test_indirect2(min, max):
    assert min >= max


# 只將max對應的實參重定向fixture中,min使用的後面的資料,max使用的是fixture的資料
@pytest.mark.parametrize("min, max", [(1, 2), (3, 4)], indirect=["max"])
def test_indirect3(min, max):
    assert min == max

indirect=True,min和max對應的實參重定向到同名的fixture中,min和max使用的是fixture的資料。

indirect=['max'],只將max對應的實參重定向到fixture中,min使用的是後面的資料,max使用的是fixture的資料。

其實這是一種間接引數化的方式,當indirect=True時,允許在將值傳遞給測試之前使用接收值的fixture對測試進行引數化。

4.7ids引數

ids引數就是id,因為與關鍵字雷同所以不能用,因此改成ids。通常不寫ids時每次不同資料直接顯示,也就是資料本身,如果定義ids值,則顯示的就是這個值。大家可以透過在ids中寫內容來標記我們的測試要點。通常我們在測試時分測試數字、字母、邊界值等,

因此我們可以透過對這個引數的設定檢查是不是覆蓋全面。例如第1個資料是數字,第2個資料是中文,第3個資料是特殊字元。這樣在報告中看到結果就知道是否測試完整。

ids是一個可執行物件,用於生成測試id,或者一個列表/元組,指明所有新增用例的測試id。這些id可用於-k選擇要執行的特定用例,當某個用例失敗時,它們還將識別該特定用例。執行pytest--collect-only將顯示生成的id。

4.7.1 ids的長度

如果使用列表/元組直接指明測試id,那麼它的長度等於argvalues的長度。

程式碼如下:

import pytest


@pytest.mark.parametrize("input, expected", [(1, 2), (3, 4)], ids=["first", "second"])
def test_ids_1(input, expected):
    pass

執行結果:

pytest的資料驅動和引數傳遞

input引數的id是first,第1次的值是1,第2次的值是3,expected引數的id是second,第1次的值是2,第2次的值是4。

4.7.2 ids相同

如果測試id相同,pytest則會在後面自動新增索引,例如[num0]和[num1]。

@pytest.mark.parametrize("input, expected", [(1, 2), (3, 4)], ids=["num", "num"])
def test_ids_2(input, expected):
    pass

執行結果:

pytest的資料驅動和引數傳遞

4.7.3 ids中使用中文

測試ID中可以使用中文,預設顯示的是位元組序列。

@pytest.mark.parametrize("input, expected", [(1, 2), (3, 4)], ids=["num", "中文"])
def test_ids_3(input, expected):
    pass

收集到的測試ID如下:

pytest的資料驅動和引數傳遞

從上面的結果可以看出,期望顯示“中文”,但實際上顯示的是\u4e2d\u6587。如何解決此問題?

原始碼如下:

pytest的資料驅動和引數傳遞

解決中文亂碼,可以在pytest.ini中將disable_test_id_escaping_and_forfeit_all_rights_to_community_support 選 項 設 置 為True。

程式碼如下:

pytest的資料驅動和引數傳遞

再次收集到的測試ID如下:

pytest的資料驅動和引數傳遞

4.7.4 透過函式生成ids

import pytest


def idfn(val):
    return val+1


@pytest.mark.parametrize("input, expected", [(1, 2), (3, 4)], ids=idfn)
def test_ids_4(input, expected):
    pass

執行結果顯示如下:

pytest的資料驅動和引數傳遞

透過上面的例子不難看出,對於一個具體的argvalues引數(1,2)來講,它被拆分為1和2分別傳遞給idfn,並將返回值透過-符號連線在一起,以此作為一個測試id返回,而不是將(1,2)作為一個整體傳入。

原始碼如下:

pytest的資料驅動和引數傳遞

先透過zip(parameterset.values,argnames)將argnames和argvalues的值一一對應,再將處理過的返回值透過"-".join(this_id)連線。

4.7.5 ids的覆蓋

假設已經透過pytest.param指定了id屬性,那麼將會覆蓋ids中對應的測試id。

程式碼如下:

pytest的資料驅動和引數傳遞

執行結果如下:

pytest的資料驅動和引數傳遞

測試id是id_via_pytest_param,而不是second。

4.7.6 ids的作用

ids最主要的作用就是更進一步細化測試用例,區分不同的測試場景,為有針對性的執行測試提供了一種新方法。

例如,對於以下測試用例,可以透過-k'Non-Windows'選項,只執行和Non-Windows相關的場景。

程式碼如下:

import pytest


@pytest.mark.parametrize("input, expected", [
    pytest.param(1, 2, id="windows"),
    pytest.param(3, 4, id="windows"),
    pytest.param(5, 6, id="no-windows")
])
def test_ids6(input, expected):
    pass

執行結果:

pytest的資料驅動和引數傳遞

4.8 scope引數

scope引數宣告argnames中引數的作用域,並透過對應的argvalues例項劃分測試用例,進而影響測試用例的收集順序。

4.8.1 module級別

如果我們顯式地指明scope引數,例如,將引數作用域宣告為模組級別,這樣設定後測試方法會進行一起統籌,也就是執行的順序是先執行所有測試方法的第一組資料,再整體執行第二組資料,直到執行完成。

程式碼如下:

import pytest


@pytest.mark.parametrize("test_input, expected", [(1, 2), (3, 4)], scope="module")
def test_scopt1(test_input, expected):
    pass


@pytest.mark.parametrize("test_input, expected", [(1, 2), (3, 4)], scope="module")
def test_scopt2(test_input, expected):
    pass

執行結果:
pytest的資料驅動和引數傳遞

當未將scope設定為module時,預設的收集順序是按測試方法的先後執行的。也就是先執行第一個測試方法中的所有資料,再執行第二測試方法中的所有資料。

pytest的資料驅動和引數傳遞

4.8.2 未指定scope

在scope未指定的情況下(或者scope=None),當indirect被設定為True或者包含所有的argnames引數時,作用域為所有fixture作用域的最小範圍,否則,其永遠為function。

import pytest


@pytest.fixture(scope="module")
def test_input(request):
    pass


@pytest.fixture(scope="module")
def expected(request):
    pass


@pytest.mark.parametrize("test_input, expected", [(1, 2), (3, 4)], indirect=True)
def test_scopt1(test_input, expected):
    pass


@pytest.mark.parametrize("test_input, expected", [(1, 2), (3, 4)], indirect=True)
def test_scopt2(test_input, expected):
    pass

test_input和expected的作用域都是module,所以引數的作用域也是module

結果如下:

pytest的資料驅動和引數傳遞

4.9 pytest_generate_tests鉤子方法

pytest實現引數化有3種方式:
·pytest.fixture()使用fixture傳params引數實現引數化;
·@pytest.mark.parametrize允許在測試函式或類中定義多組引數;
·pytest_generate_tests允許定義自定義引數化方案或擴充套件。
本節簡單介紹自定義引數化方案。pytest_generate_tests在測試用例引數化收集前呼叫此鉤子函式,根據測試配置或定義測試函式的類或模組中指定的引數值生成測試用例,可以使用此鉤子實現自定義引數化方案或擴充套件。

有時可能要實現自己的引數化方案或實現某種動態性來確定fixture的引數或範圍,因此,可以使用pytest_generate_tests在收集測試函式時呼叫的鉤子。透過傳入的metafunc物件,可以檢查請求的測試上下文,最重要的一點是,可以呼叫metafunc.parametrize()引起引數化。

我們先看一看原始碼中是怎麼使用這種方法的。

原始碼如下:

pytest的資料驅動和引數傳遞

首 先 , 它 檢 查 了 parametrize 的 拼 寫 錯 誤 , 如 果 不 小 心 將 parametrize 寫 成 了["parameterize","parametrise","parameterise"]中的一個,pytest會返回一個異常,並提示正確的單詞,然後迴圈遍歷所有的parametrize的標記,

並呼叫metafunc.parametrize方法。例如,假設我們要執行一個測試,並接收透過新的pytest命令列選項設定的字串輸入。我們首先需要編寫一個接收stringinput函式引數的簡單測試。我們檢查給定的stringinput是否只由字母組成,

但是我們並沒有為其打上parametrize標記,所以stringinput被認為是一個fixture。

程式碼如下

pytest的資料驅動和引數傳遞

現在,我們期望把stringinput當成一個普通的引數,並且從命令列賦值。

首先,我們定義一個命令列選項。

程式碼如下:

pytest的資料驅動和引數傳遞

然 後 , 我 們 通 過 pytest_generate_tests 方 法 , 將 stringinput 的 行 為 由 fixture 改 成parametrize。

程式碼如下:

pytest的資料驅動和引數傳遞

最後,我們可以透過--stringinput命令列選項為stringinput引數賦值。

程式碼如下:

pytest的資料驅動和引數傳遞

如果我們不加--stringinput選項,相當於parametrize的argnames中的引數沒有接收到任何的實參,那麼測試用例的結果將會被置為SKIPPED。

pytest的資料驅動和引數傳遞

不管是metafunc.parametrize方法還是@pytest.mark.parametrize標記,它們的引數(argnames)不能是重複的,否則會產生一個錯誤:ValueError:duplicate 'stringinput'。

相關文章