天天看點

Pytest fixture及conftest詳解

作者:大剛測試開發實戰

前言

fixture是在測試函數運作前後,由pytest執行的外殼函數。fixture中的代碼可以定制,滿足多變的測試需求,包括定義傳入測試中的資料集、配置測試前系統的初始狀态、為批量測試提供資料源等等。fixture是pytest的精髓所在,類似unittest中setup/teardown,但是比它們要強大、靈活很多,它的優勢是可以跨檔案共享。

一、Pytest fixture

1.pytest fixture幾個關鍵特性

  • 有獨立的命名,并通過聲明它們從測試函數、子產品、類或整個項目中的使用來激活
  • 按子產品化的方式實作,每個fixture都可以互相調用
  • fixture可以實作unittest不能實作的功能,比如unittest中的測試用例和測試用例之間是無法傳遞參數和資料的,但是fixture卻可以解決這個問題
  • fixture的範圍從簡單的單元擴充到複雜的功能測試,允許根據配置群組件選項對fixture和測試用例進行參數化

2.Pytest fixture定義

  • 定義fixture跟定義普通函數差不多,唯一差別就是在函數上加個裝飾器@pytest.fixture(),fixture命名不要用test_開頭,跟用例區分開。用例才是test_開頭的命名;
  • fixture裝飾器裡的scope有四個級别的參數:function(不寫預設這個)、class、module、session;
  • fixture可以有傳回值,如果沒有return,預設會是None;用例調用fixture的傳回值,就是直接把fixture的函數名稱作為參數傳入;
  • fixture可以傳回一個元組、清單或字典;
  • 測試用例可傳單個、多個fixture參數;
  • fixture與fixture間可互相調用;

3.Pytest fixture用法

1)用法一:作為參數使用

fixture的名字直接作為測試用例的參數,用例調用fixture的傳回值,直接将fixture的函數名稱當做變量名稱;如果用例需要用到多個fixture的傳回資料,fixture也可以傳回一個元祖,list或字典,然後從裡面取出對應資料。

① 将fixture函數作為參數傳遞給測試用例

@pytest.fixture()
def login():
    print("this is login fixture")
    user = "chen"
    pwd = 123456
    return user, pwd

def test_login(login):
    """将fixture修飾的login函數作為參數傳遞給本用例"""
    print(login)
    assert login[0] == "chen"
    assert login[1] == 123456
    assert "chen" in str(login)           

② 同一個用例中傳入多個fixture函數

@pytest.fixture()
def user():
    user = "cris"
    return user

@pytest.fixture()
def pwd():
    pwd = "123456"
    return pwd

def test_trans_fixture(user, pwd):
    """同一條用例中傳入多個fixture函數"""
    print(user, pwd)
    assert "cris" in str(user)
    assert pwd == "123456"           

③ fixture函數之間的互相傳遞

@pytest.fixture()
def user2():
    user = "cris"
    return user

@pytest.fixture()
def login_info(user2):
    """fixture與fixture函數之間的互相傳遞"""
    pwd = "e10adc3949ba59abbe56e057f20f883e"
    return user2, pwd

def test_assert_login_info(login_info):
    print(login_info)
    print(type(login_info))
    assert login_info[0] == "cris"
    assert login_info[1] == "e10adc3949ba59abbe56e057f20f883e"           

2)用法二:提供靈活的類似setup和teardown功能

Pytest的fixture另一個強大的功能就是在函數執行前後增加操作,類似setup和teardown操作,但是比setup和teardown的操作更加靈活;具體使用方式是同樣定義一個函數,然後用裝飾器标記為fixture,然後在此函數中使用一個yield語句,yield語句之前的就會在測試用例之前使用,yield之後的語句就會在測試用例執行完成之後再執行。

@pytest.fixture()
def run_function():
    print("run before function...")
    yield
    print("run after function...")

def test_run_1(run_function):
    print("case 1")

def test_run_2():
    print("case 2")

def test_run_3(run_function):
    print("case 3")           

運作結果如下:

Pytest fixture及conftest詳解

常見的應用場景:@pytest.fixture可以用在selenium中測試用例執行前後打開、關閉浏覽器的操作:

@pytest.fixture()
def fixture_driver():
    driver = webdriver.Chrome()
    yield driver
    driver.quit()

def test_baidu(fixture_driver):
    driver = fixture_driver
    driver.get("http://www.baidu.com")
    driver.find_element_by_id('kw').send_keys("python fixture")
    driver.find_element_by_id('su').click()           

3)用法三:利用pytest.mark.usefixtures疊加調用多個fixture

如果一個方法或者一個class用例想要同時調用多個fixture,可以使用@pytest.mark.usefixtures()進行疊加。注意疊加順序,先執行的放底層,後執行的放上層。需注意:

① 與直接傳入fixture不同的是,@pytest.mark.usefixtures無法擷取到被fixture裝飾的函數的傳回值;

② @pytest.mark.usefixtures的使用場景是:被測試函數需要多個fixture做前後置工作時使用;

@pytest.fixture
def func_1():
    print("用例前置操作---1")
    yield
    print("用例後置操作---1")

@pytest.fixture
def func_2():
    print("用例前置操作---2")
    yield
    print("用例後置操作---2")

@pytest.fixture
def func_3():
    print("用例前置操作---3")
    yield
    print("用例後置操作---3")

@pytest.mark.usefixtures("func_3")  # 最後執行func_3
@pytest.mark.usefixtures("func_2")  # 再執行func_1
@pytest.mark.usefixtures("func_1")  # 先執行func_1
def test_func():
    print("這是測試用例")           

執行結果:

Pytest fixture及conftest詳解

4)用法四:fixture自動使用autouse=True

當用例很多的時候,每次都傳這個參數,會很麻煩。fixture裡面有個參數autouse,預設是False沒開啟的,可以設定為True開啟自動使用fixture功能,這樣用例就不用每次都去傳參了,autouse設定為True,自動調用fixture功能。所有用例都會生效,包括類中的測試用例和類以外的測試用例。

@pytest.fixture(autouse=True, scope="function")
def func_auto():
    """autouse為True時,會作用于每一條用例"""
    print("\n---用例前置操作---")
    yield
    print("---用例後置操作---")

# func_auto函數的autouse=True時,無論是否使用usefixtures引用func_auto,都會執行func_auto
@pytest.mark.usefixtures("func_auto")
def test_01():
    print("case 1")

def test_02():
    print("case 2")

class Test:
    def test_03(self):
        print("case 3")           

執行結果:

Pytest fixture及conftest詳解

4.Pytest fixture四種作用域

fixture(scope='function',params=None,autouse=False,ids=None,name=None)           

fixture裡面有個scope參數可以控制fixture的作用範圍:

  • function:每一個函數或方法都會調用
  • class:每一個類調用一次,一個類中可以有多個方法
  • module:每一個.py檔案調用一次,該檔案内又有多個function和class
  • session:多個檔案調用一次,可以跨.py檔案調用(通常這個級别會結合conftest.py檔案使用)

1)function級别

function預設模式為@pytest.fixture() 函數級别,即scope="function",scope可以不寫。每一個函數或方法都會調用,每個測試用例執行前都會執行一次function級别的fixture。

# @pytest.fixture(scope="function")等價于@pytest.fixture()
@pytest.fixture(scope="function")
def func_auto():
    """用例級别fixture,作用域單個用例"""
    print("\n---function級别的用例前置操作---")
    yield
    print("---function級别的用例後置操作---")

# test_01會引用func_auto函數,test_02沒有用修飾器修飾,故不會引用
def test_func_auto_fixture_1(func_auto):
    print("func 1 print")

def test_func_auto_fixture_2():
    print("func 2 print")           

2)class級别

fixture的scope值還可以是class,此時則fixture定義的動作就會在測試類class的所有用例之前和之後運作,需注意:測試類中隻要有一個測試用例的參數中使用了class級别的fixture,則在整個測試類的所有測試用例都會調用fixture函數

① 用例類中的測試用例調用fixture

執行fixture定義的動作,以及此測試類的所有用例結束後同樣要運作fixture指定的動作

@pytest.fixture(scope="class")
def class_auto():
    """類級别fixture,作用域整個類"""
    print("\n---class級别的用例前置操作---")
    yield
    print("---class級别的用例後置操作---")

class TestClassAutoFixture:
    # class級别的fixture任意一個用例引用即可
    def test_class_auto_fixture_1(self, class_auto):
        print("class 1 print")

    def test_class_auto_fixture_2(self):
        print("class 1 print")           

測試類中的第1條測試用例引用了fixture修飾的函數,則整個測試類的所有測試用例都會執行fixture函數的前置操作,在所有用例執行完成後,都會執行fixture函數的後置操作。

② 用例類外的測試用例調用fixture

如果在類外的函數中去使用class級别的fixture,則此時在測試類外每個測試用例中,fixture跟function級别的fixture作用是一緻的,即在類外的函數中引用了class級别的fixture,則在此函數之前和之後同樣去執行fixture定義的對應的操作。

def test_class_auto_fixture(class_auto):
    print("class 1 print")           

如下圖所示,測試類外的函數引用了class級别的fixture,則它的作用會等同于function級别的fixture,運作結果如下:

Pytest fixture及conftest詳解

3)module級别

在Python中module即.py檔案,當fixture定義為module時,則此fixture将在目前檔案中起作用。這裡需要特别說明的是,當fixture的scope定義為module時,隻要目前檔案中有一個測試用例使用了fixture,不管這個用例是在類外,還是在類中,都會在目前檔案(子產品)的所有測試用例執行之前去執行fixture定義的行為以及目前檔案的所有用例結束之後同樣去執行fixture定義的對應操作。

@pytest.fixture(scope="module")
def module_auto():
    """作用于整個py檔案"""
    print("\n---module級别的用例前置操作---")
    yield
    print("---module級别的用例後置操作---")
    
# 測試類外和測試類内的函數方法都調用了module級别的fixture,但整個py檔案隻會生效一次fixture。
def test_module_scope_out_class(module_auto):
    print("case scope 01")
    
class TestScope1:
    def test_scope_01(self):
        print("case scope 01")

    def test_scope_02(self, module_auto):
        print("case scope 02")

    def test_scope_03(self):
        print("case scope 03")           

若類中的方法分别調用了class級别的fixture和module級别的fixture,則會兩個fixture都生效:

# 順序在前面fixture會先執行
def test_scope_01(self, module_auto, class_auto): 
    print("case scope 01")           

若類中的方法同時調用了function級别、class級别、module級别的fixture,則3種fixture會同時生效:

# 順序在前面fixture會先執行
def test_scope_02(self, module_auto, class_auto, func_auto):  
    print("case scope 02")           

4)session級别(使用conftest.py共享fixture)

當fixture的scope定義為session時,是指在目前目錄下的所有用例之前和之後執行fixture對應的操作

fixture為session級别是可以跨.py子產品調用的,也就是當我們有多個.py檔案的用例的時候,如果多個用例隻需調用一次fixture,那就可以設定為scope="session",并且寫到conftest.py檔案裡

使用方式:

① 定義測試用例檔案

② 在指定目錄下建立conftest.py(固定命名,不可修改)檔案,然後在conftest.py檔案中定義fixture方法,将scope指定為session,此時在目前目錄下隻要有一個用例使用了此fixture,則就會在目前目錄下所有用例之前和之後會執行fixture定義的對應的操作。

@pytest.fixture(scope="session", )
def session_auto():
    """session級别的fixture,針對該目錄下的所有用例都生效"""
    print("\n---session級别的用例前置操作---")
    yield
    print("---session級别的用例後置操作---")           

定義了session級别的fixture,存放于該用例檔案的同一個目錄下的conftest.py檔案中,該目錄下的任一用例檔案中的任一測試用例,引用了這個session級别的fixture,則這個session級别的fixture會針對這整個用例檔案會生效。若存放在根目錄下,則針對整個工程的所有用例都會生效。

class TestSessionAutoFixture:
    # session級别的fixture任意一個用例引用即可
    def test_session_auto_fixture_1(self, session_auto):
        print("session 1 print")

    def test_session_auto_fixture_2(self):
        print("session 1 print")


def test_session_auto_fixture():
    print("session 1 print")           

運作結果如下:

Pytest fixture及conftest詳解

5.Pytest fixture其他參數用法

1)ids參數-修改用例結果名稱

@pytest.mark.parametrize() 還提供了第三個 ids 參數來自定義顯示結果。

stars = ["劉德華", "張學友", "黎明", "郭富城"]
# 利用清單生成式生成一個用例名稱的清單
ids = [f"test-case-{d}" for d in range(len(stars))]

@pytest.mark.parametrize("name", stars, ids=ids)
def test_multi_param(name):
    print(f"my name is {name}")           

注:ids生成的用例名稱數量一定要和用例數量一緻,否則會報錯,執行結果如下:

Pytest fixture及conftest詳解

2)name參數-重命名fixture函數名稱

@pytest.fixture(name="rename_get_user_info")
def get_user_info():
    user_name = "周潤發"
    print(user_name)

# 此處需傳入重命名後的fixture函數名
@pytest.mark.usefixtures("rename_get_user_info")
def test_parametrize_by_use_fixtures():
    """通過usefixtures裝飾器傳入fixture"""
    print(f"test parametrize use fixtures")
    
def test_parametrize_by_fixture_name(rename_get_user_info):
    """将fixture函數名作為形參傳入"""
    print(f"test parametrize use fixtures")           

3)params參數-提供傳回值供測試函數調用

示例一

@pytest.fixture(params=[{"name": "周潤發"}, {"age": 61}, {"height": 183}])
def fix_func(request):  # request為内建fixture
    # 使用request.param作為傳回值供測試函數調用,params的參數清單中包含了做少元素,該fixture就會被調用幾次,分别作用在每個測試函數上
    return request.param  # request.param為固定寫法

def test_fix_func(fix_func):
    print(f"fixture函數fix_func的傳回值為:{fix_func}")
    
    """列印結果如下:
    fixture函數fix_func的傳回值為:{'name': '周潤發'}
    fixture函數fix_func的傳回值為:{'age': 61}
    fixture函數fix_func的傳回值為:{'height': 183}
    """           

示例二

params = [
    {"case_id": 1, "case_title": "驗證正常添加車輛", "car_name": "蘇C99688", "car_type": 1, "origin": 1, "expected": "200"},
    {"case_id": 2, "case_title": "驗證添加重複車輛", "car_name": "蘇C99688", "car_type": 1, "origin": 1, "expected": "500"},
    {"case_id": 3, "case_title": "驗證車牌号為空", "car_name": "", "car_type": 2, "origin": 1, "expected": "500"}]

@pytest.fixture(params=params)
def add_car_params(request):
    return request.param

def test_add_car(add_car_params):
    print(f"{add_car_params['case_id']}-{add_car_params['case_title']}-{add_car_params['car_name']}")
    
    """
    運作結果如下:
    1-驗證正常添加車輛-蘇C99688
    2-驗證添加重複車輛-蘇C99688
    3-驗證車牌号為空-
    """           
Pytest fixture及conftest詳解

6.内置fixture

1)tmpdir和tmpdir_factory

内置的tmpdir和tmpdir_factory負責在測試開始運作前建立臨時檔案目錄,并在測試結束後删除。如果測試代碼要對檔案進行讀/寫操作,那麼可以使用tmpdir或tmpdir_factory來建立檔案或目錄。單個測試使用tmpdir,多個測試使用tmpdir_factory。tmpdir的作用範圍是函數級别,tmpdir_factory的作用範圍是會話級别。

def test_tmpdir(tmpdir):
    # tmpdir already has a path name associated with it
    # join() extends the path to include a filename
    # the file is created when it's written to
    a_file = tmpdir.join('something.txt')

    # you can create directories
    a_sub_dir = tmpdir.mkdir('anything')

    # you can create files in directories (created when written)
    another_file = a_sub_dir.join('something_else.txt')

    # this write creates 'something.txt'
    a_file.write('contents may settle during shipping')

    # this write creates 'anything/something_else.txt'
    another_file.write('something different')

    # you can read the files as well
    assert a_file.read() == 'contents may settle during shipping'
    assert another_file.read() == 'something different'


def test_tmpdir_factory(tmpdir_factory):
    # you should start with making a directory
    # a_dir acts like the object returned from the tmpdir fixture
    a_dir = tmpdir_factory.mktemp('mydir')

    # base_temp will be the parent dir of 'mydir'
    # you don't have to use getbasetemp()
    # using it here just to show that it's available
    base_temp = tmpdir_factory.getbasetemp()
    print('base:', base_temp)

    # the rest of this test looks the same as the 'test_tmpdir()'
    # example except I'm using a_dir instead of tmpdir

    a_file = a_dir.join('something.txt')
    a_sub_dir = a_dir.mkdir('anything')
    another_file = a_sub_dir.join('something_else.txt')

    a_file.write('contents may settle during shipping')
    another_file.write('something different')

    assert a_file.read() == 'contents may settle during shipping'
    assert another_file.read() == 'something different'           

2)pytestconfig

内置的pytestconfig可以通過指令行參數、選項、配置檔案、插件、運作目錄等方式來控制pytest。pytestconfig是request.config的快捷方式,它在pytest文檔裡有時候被稱為“pytest配置對象”。

要了解pytestconfig如何工作,可以添加一個自定義的指令行選項,然後在測試中讀取該選項。

def pytest_addoption(parser):
    """"添加一個指令行選項"""
    parser.addoption(
        "--env", default="test", choices=["dev", "test", "pre"], help="enviroment parameter")           

以pytest_addoption添加的指令行選項必須通過插件來實作,或者在項目頂層目錄的conftest.py檔案中完成。它所在的conftest.py不能處于測試子目錄下。

上述是一個傳入測試環境的指令行選項,接下來可以在測試用例中使用這些選項。

def test_option(pytestconfig):
    print('the current environment is:', pytestconfig.getoption('env'))           
# 運作測試
pytest -s -q test_config.py::test_option           

由于前面的pytest_addoption中定義的env的預設參數是test,是以通過pytestconfig.getoption擷取到的env的值就是test:

Pytest fixture及conftest詳解

3)其他内置fixture

  • cache:作用是存儲一段測試會話的資訊,在下一段測試會話中使用;
  • capsys:capsys 有兩個功能:允許使用代碼讀取 stdout 和 stderr;可以臨時禁制抓取日志輸出;
  • monkeypatch:可以在運作期間對類或子產品進行動态修改。在測試中,monkey patch 常用于替換被測試代碼的部分運作環境,或者将輸入依賴或輸出依賴替換成更容易測試的對象或函數;
  • doctest_namespace:doctest 子產品是 Python 标準庫的一部分,借助它,可以在函數的文檔字元串中放入示例代碼,并通過測試確定有效。你可以使用 --doctest-modules 辨別搜尋并運作 doctest 測試用例;
  • recwarn:可以用來檢查待測代碼産生的警告資訊;recwarn 的值就像是一個警告資訊清單,清單裡的每個警告資訊都有4個屬性 category、message、filename、lineno。警告資訊在測試開始後收集,如果你在意的警告資訊出現在測試尾部,則可以在資訊收集前使用 recwarn.clear() 清除不需要的内容。除了 recwarn,pytest 還可以使用 pytest.warns() 來檢查警告資訊。

二、Pytest conftest全局作用檔案詳解

Pytest支援在測試的目錄中,建立conftest.py檔案,進行全局配置。

conftest.py檔案須知:

  1. 可以跨.py檔案調用,有多個.py檔案調用時,可讓conftest.py隻調用了一次fixture,或調用多次fixture;
  2. conftest.py與運作的用例要在同一個pakage下,并且有__init__.py檔案;
  3. 不需要import導入conftest.py,pytest用例會自動識别該檔案,放到項目的根目錄下就可以全局目錄調用了,如果放到某個package下,那就在package内有效,可有多個conftest.py;
  4. conftest.py配置腳本名稱是固定的,不能改名稱;
  5. conftest.py檔案不能被其他檔案導入;
  6. 所有同目錄測試檔案運作前都會執行conftest.py檔案;

繼續閱讀