4岔帽、pytest-fixtures:明確的玫鸟、模塊化的和可擴展的

pytest fixtures的目的是提供一個固定的基線,使測試可以在此基礎(chǔ)上可靠地犀勒、重復(fù)地執(zhí)行屎飘;對比xUnit經(jīng)典的setup/teardown形式,它在以下方面有了明顯的改進:

  • fixture擁有一個明確的名稱贾费,通過聲明使其能夠在函數(shù)钦购、類、模塊铸本,甚至整個測試會話中被激活使用肮雨;
  • fixture以一種模塊化的方式實現(xiàn)。因為每一個fixture的名字都能觸發(fā)一個fixture函數(shù)箱玷,而這個函數(shù)本身又能調(diào)用其它的fixture怨规;
  • fixture的管理從簡單的單元測試擴展到復(fù)雜的功能測試,允許通過配置和組件選項參數(shù)化fixture和測試用例锡足,或者跨功能波丰、類、模塊舶得,甚至整個測試會話復(fù)用fixture掰烟;

此外,pytest繼續(xù)支持經(jīng)典的xUnit風(fēng)格的測試沐批。你可以根據(jù)自己的喜好纫骑,混合使用兩種風(fēng)格,或者逐漸過渡到新的風(fēng)格九孩。你也可以從已有的unittest.TestCase或者nose項目中執(zhí)行測試先馆;

1. fixture:作為形參使用

測試用例可以接收fixture的名字作為入?yún)ⅲ鋵崊⑹菍?yīng)的fixture函數(shù)的返回值躺彬。通過@pytest.fixture裝飾器可以注冊一個fixture煤墙;

我們來看一個簡單的測試模塊,它包含一個fixture和一個使用它的測試用例:

# src/chapter-4/test_smtpsimple.py

import pytest


@pytest.fixture
def smtp_connection():
    import smtplib

    return smtplib.SMTP("smtp.163.com", 25, timeout=5)


def test_ehlo(smtp_connection):
    response, _ = smtp_connection.ehlo()
    assert response == 250
    assert 0  # 為了展示宪拥,強制置為失敗

這里仿野,test_ehlo有一個形參smtp_connection,和上面定義的fixture函數(shù)同名她君;

執(zhí)行:

$ pipenv run pytest -q src/chapter-4/test_smtpsimple.py 
F                                                                 [100%]
=============================== FAILURES ================================
_______________________________ test_ehlo _______________________________

smtp_connection = <smtplib.SMTP object at 0x105992d68>

    def test_ehlo(smtp_connection):
        response, _ = smtp_connection.ehlo()
        assert response == 250
>       assert 0  # 為了展示脚作,強制置為失敗
E       assert 0

src/chapter-4/test_smtpsimple.py:35: AssertionError
1 failed in 0.17s

執(zhí)行的過程如下:

  • pytest收集到測試用例test_ehlo,其有一個形參smtp_connection缔刹,pytest查找到一個同名的已經(jīng)注冊的fixture鳖枕;
  • 執(zhí)行smtp_connection()創(chuàng)建一個smtp_connection實例<smtplib.SMTP object at 0x105992d68>作為test_ehlo的實參魄梯;
  • 執(zhí)行test_ehlo(<smtplib.SMTP object at 0x105992d68>)

如果你不小心拼寫出錯宾符,或者調(diào)用了一個未注冊的fixture酿秸,你會得到一個fixture <...> not found的錯誤,并告訴你目前所有可用的fixture魏烫,如下:

$ pipenv run pytest -q src/chapter-4/test_smtpsimple.py 
E                                                                 [100%]
================================ ERRORS =================================
______________________ ERROR at setup of test_ehlo ______________________
file /Users/yaomeng/Private/Projects/pytest-chinese-doc/src/chapter-4/test_smtpsimple.py, line 32
  def test_ehlo(smtp_connectio):
E       fixture 'smtp_connectio' not found
>       available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, smtp_connection, smtp_connection_package, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
>       use 'pytest --fixtures [testpath]' for help on them.

/Users/yaomeng/Private/Projects/pytest-chinese-doc/src/chapter-4/test_smtpsimple.py:32
1 error in 0.02s

注意:

你也可以使用如下調(diào)用方式:

pytest --fixtures [testpath]

它會幫助你顯示所有可用的 fixture辣苏;

但是,對于_開頭的fixture哄褒,需要加上-v選項稀蟋;

2. fixture:一個典型的依賴注入的實踐

fixture允許測試用例可以輕松的接收和處理特定的需要預(yù)初始化操作的應(yīng)用對象,而不用過分關(guān)心導(dǎo)入/設(shè)置/清理的細節(jié)呐赡;這是一個典型的依賴注入的實踐退客,其中,fixture扮演者注入者(injector)的角色链嘀,而測試用例扮演者消費者(client)的角色萌狂;

以上一章的例子來說明:test_ehlo測試用例需要一個smtp_connection的連接對象來做測試,它只關(guān)心這個連接是否有效和可達怀泊,并不關(guān)心它的創(chuàng)建過程茫藏。smtp_connectiontest_ehlo來說,就是一個需要預(yù)初始化操作的應(yīng)用對象霹琼,而這個預(yù)處理操作是在fixture中完成的务傲;簡而言之,test_ehlo說:“我需要一個SMTP連接對象枣申∈燮希”,然后忠藤,pytest就給了它一個挟伙,就這么簡單。

關(guān)于依賴注入的解釋熄驼,可以看看Stackflow上這個問題的高票回答如何向一個5歲的孩子解釋依賴注入像寒?

When you go and get things out of the refrigerator for yourself, you can cause problems. You might leave the door open, you might get something Mommy or Daddy doesn't want you to have. You might even be looking for something we don't even have or which has expired.

What you should be doing is stating a need, "I need something to drink with lunch," and then we will make sure you have something when you sit down to eat.

更詳細的資料可以看看維基百科Dependency injection烘豹;

3. conftest.py:共享fixture實例

如果你想在多個測試模塊中共享同一個fixture實例瓜贾,那么你可以把這個fixture移動到conftest.py文件中。在測試模塊中你不需要手動的導(dǎo)入它携悯,pytest會自動發(fā)現(xiàn)祭芦,fixture的查找的順序是:測試類、測試模塊憔鬼、conftest.py龟劲、最后是內(nèi)置和第三方的插件胃夏;

你還可以利用conftest.py文件的這個特性為每個目錄實現(xiàn)一個本地化的插件

4. 共享測試數(shù)據(jù)

如果你想多個測試共享同樣的測試數(shù)據(jù)文件昌跌,我們有兩個好方法實現(xiàn)這個:

  • 把這些數(shù)據(jù)加載到fixture中仰禀,測試中再使用這些fixture
  • 把這些數(shù)據(jù)文件放到tests文件夾中蚕愤,一些第三方的插件能幫助你管理這方面的測試答恶,例如:pytest-datadirpytest-datafiles

5. 作用域:在跨類的萍诱、模塊的或整個測試會話的用例中悬嗓,共享fixture實例

需要使用到網(wǎng)絡(luò)接入的fixture往往依賴于網(wǎng)絡(luò)的連通性,并且創(chuàng)建過程一般都非常耗時裕坊;

我們來擴展一下上述示例(src/chapter-4/test_smtpsimple.py):在@pytest.fixture裝飾器中添加scope='module'參數(shù)包竹,使每個測試模塊只調(diào)用一次smtp_connection(默認每個用例都會調(diào)用一次),這樣模塊中的所有測試用例將會共享同一個fixture實例籍凝;其中周瞎,scope參數(shù)可能的值都有:function(默認值)、class静浴、module堰氓、packagesession

首先苹享,我們把smtp_connection()提取到conftest.py文件中:

# src/chapter-4/conftest.py


import pytest
import smtplib


@pytest.fixture(scope='module')
def smtp_connection():
  return smtplib.SMTP("smtp.163.com", 25, timeout=5)

然后双絮,在相同的目錄下,新建一個測試模塊test_module.py得问,將smtp_connection作為形參傳入每個測試用例囤攀,它們共享同一個smtp_connection()的返回值:

# src/chapter-4/test_module.py


def test_ehlo(smtp_connection):
    response, _ = smtp_connection.ehlo()
    assert response == 250
    smtp_connection.extra_attr = 'test'
    assert 0  # 為了展示,強制置為失敗


def test_noop(smtp_connection):
    response, _ = smtp_connection.noop()
    assert response == 250
    assert smtp_connection.extra_attr == 0  # 為了展示宫纬,強制置為失敗

最后焚挠,讓我們來執(zhí)行這個測試模塊:

pipenv run pytest -q src/chapter-4/test_module.py 
FF                                                                [100%]
=============================== FAILURES ================================
_______________________________ test_ehlo _______________________________

smtp_connection = <smtplib.SMTP object at 0x107193c50>

    def test_ehlo(smtp_connection):
        response, _ = smtp_connection.ehlo()
        assert response == 250
        smtp_connection.extra_attr = 'test'
>       assert 0  # 為了展示,強制置為失敗
E       assert 0

src/chapter-4/test_module.py:27: AssertionError
_______________________________ test_noop _______________________________

smtp_connection = <smtplib.SMTP object at 0x107193c50>

    def test_noop(smtp_connection):
        response, _ = smtp_connection.noop()
        assert response == 250
>       assert smtp_connection.extra_attr == 0
E       AssertionError: assert 'test' == 0
E        +  where 'test' = <smtplib.SMTP object at 0x107193c50>.extra_attr

src/chapter-4/test_module.py:33: AssertionError
2 failed in 0.72s

可以看到:

  • 兩個測試用例使用的smtp_connection實例都是<smtplib.SMTP object at 0x107193c50>漓骚,說明smtp_connection只被調(diào)用了一次蝌衔;
  • 在前一個用例test_ehlo中修改smtp_connection實例(上述例子中,為smtp_connection添加extra_attr屬性)蝌蹂,也會反映到test_noop用例中噩斟;

如果你期望擁有一個會話級別作用域的fixture,可以簡單的將其聲明為:

@pytest.fixture(scope='session')
def smtp_connection():
  return smtplib.SMTP("smtp.163.com", 25, timeout=5)

注意:

pytest每次只緩存一個fixture實例孤个,當使用參數(shù)化的fixture時剃允,pytest可能會在聲明的作用域內(nèi)多次調(diào)用這個fixture

5.1. package作用域(實驗性的)

在 pytest 3.7 的版本中,正式引入了package作用域斥废。

package作用域的fixture會作用于包內(nèi)的每一個測試用例:

首先椒楣,我們在src/chapter-4目錄下創(chuàng)建如下的組織:

chapter-4/
└── package_expr
    ├── __init__.py
    ├── test_module1.py
    └── test_module2.py

然后,在src/chapter-4/conftest.py中聲明一個package作用域的fixture

@pytest.fixture(scope='package')
def smtp_connection_package():
    return smtplib.SMTP("smtp.163.com", 25, timeout=5)

接著牡肉,在src/chapter-4/package_expr/test_module1.py中添加如下測試用例:

def test_ehlo_in_module1(smtp_connection_package):
    response, _ = smtp_connection_package.ehlo()
    assert response == 250
    assert 0  # 為了展示捧灰,強制置為失敗


def test_noop_in_module1(smtp_connection_package):
    response, _ = smtp_connection_package.noop()
    assert response == 250
    assert 0  # 為了展示,強制置為失敗

同樣统锤,在src/chapter-4/package_expr/test_module2.py中添加如下測試用例:

def test_ehlo_in_module2(smtp_connection_package):
    response, _ = smtp_connection_package.ehlo()
    assert response == 250
    assert 0  # 為了展示凤壁,強制置為失敗

最后,執(zhí)行src/chapter-4/package_expr下所有的測試用例:

$ pipenv run pytest -q src/chapter-4/package_expr/
FFF                                                               [100%]
=============================== FAILURES ================================
_________________________ test_ehlo_in_module1 __________________________

smtp_connection_package = <smtplib.SMTP object at 0x1028fec50>

    def test_ehlo_in_module1(smtp_connection_package):
        response, _ = smtp_connection_package.ehlo()
        assert response == 250
>       assert 0  # 為了展示跪另,強制置為失敗
E       assert 0

src/chapter-4/package_expr/test_module1.py:26: AssertionError
_________________________ test_noop_in_module1 __________________________

smtp_connection_package = <smtplib.SMTP object at 0x1028fec50>

    def test_noop_in_module1(smtp_connection_package):
        response, _ = smtp_connection_package.noop()
        assert response == 250
>       assert 0
E       assert 0

src/chapter-4/package_expr/test_module1.py:32: AssertionError
_________________________ test_ehlo_in_module2 __________________________

smtp_connection_package = <smtplib.SMTP object at 0x1028fec50>

    def test_ehlo_in_module2(smtp_connection_package):
        response, _ = smtp_connection_package.ehlo()
        assert response == 250
>       assert 0  # 為了展示拧抖,強制置為失敗
E       assert 0

src/chapter-4/package_expr/test_module2.py:26: AssertionError
3 failed in 0.45s

可以看到:

  • 雖然這三個用例在不同的模塊中,但是使用相同的fixture實例免绿,即<smtplib.SMTP object at 0x1028fec50>唧席;

注意:

  • chapter-4/package_expr可以不包含__init__.py文件,因為pytest發(fā)現(xiàn)測試用例的規(guī)則沒有強制這一點嘲驾;同樣淌哟,package_expr/的命名也不需要符合test_*或者*_test的規(guī)則;

  • 這個功能標記為實驗性的辽故,如果在其實際應(yīng)用中發(fā)現(xiàn)嚴重的bug徒仓,那么這個功能很可能被移除;

6. fixture的實例化順序

多個fixture的實例化順序誊垢,遵循以下原則:

  • 高級別作用域的(例如:session)先于低級別的作用域的(例如:class或者function)實例化掉弛;
  • 相同級別作用域的,其實例化順序遵循它們在測試用例中被聲明的順序(也就是形參的順序)喂走,或者fixture之間的相互調(diào)用關(guān)系殃饿;
  • 使能autousefixture,先于其同級別的其它fixture實例化芋肠;

我們來看一個具體的例子:

# src/chapter-4/test_order.py

import pytest

order = []


@pytest.fixture(scope="session")
def s1():
    order.append("s1")


@pytest.fixture(scope="module")
def m1():
    order.append("m1")


@pytest.fixture
def f1(f3):
    order.append("f1")


@pytest.fixture
def f3():
    order.append("f3")


@pytest.fixture(autouse=True)
def a1():
    order.append("a1")


@pytest.fixture
def f2():
    order.append("f2")


def test_order(f1, m1, f2, s1):
    assert order == ["s1", "m1", "a1", "f3", "f1", "f2"]
  • s1擁有最高級的作用域(session)乎芳,即使在測試用例test_order中最后被聲明,它也是第一個被實例化的(參照第一條原則)

  • m1擁有僅次于session級別的作用域(module)帖池,所以它是第二個被實例化的(參照第一條原則)

  • f1 f2 f3 a1同屬于function級別的作用域:

    • test_order(f1, m1, f2, s1)形參的聲明順序中奈惑,可以看出,f1f2先實例化(參照第二條原則)
    • f1的定義中又顯式的調(diào)用了f3睡汹,所以f3f1先實例化(參照第二條原則)
    • a1的定義中使能了autouse標記肴甸,所以它會在同級別的fixture之前實例化,這里也就是在f3 f1 f2之前實例化(參照第三條原則)
  • 所以這個例子fixture實例化的順序為:s1 m1 a1 f3 f1 f2

注意:

  • 除了autousefixture帮孔,需要測試用例顯示聲明(形參)雷滋,不聲明的不會被實例化;

  • 多個相同作用域的autouse fixture文兢,其實例化順序遵循fixture函數(shù)名的排序晤斩;

7. fixture的清理操作

我們期望在fixture退出作用域之前,執(zhí)行某些清理性操作(例如姆坚,關(guān)閉服務(wù)器的連接等)澳泵;

我們有以下幾種形式,實現(xiàn)這個功能:

7.1. 使用yield代替return

fixture函數(shù)中的return關(guān)鍵字替換成yield兼呵,則yield之后的代碼兔辅,就是我們要的清理操作;

我們來聲明一個包含清理操作的smtp_connection

# src/chapter-4/conftest.py

@pytest.fixture()
def smtp_connection_yield():
    smtp_connection = smtplib.SMTP("smtp.163.com", 25, timeout=5)
    yield smtp_connection
    print("關(guān)閉SMTP連接")
    smtp_connection.close()

再添加一個使用它的測試用例:

# src/chapter-4/test_smtpsimple.py

def test_ehlo_yield(smtp_connection_yield):
    response, _ = smtp_connection_yield.ehlo()
    assert response == 250
    assert 0  # 為了展示击喂,強制置為失敗

現(xiàn)在维苔,我們來執(zhí)行它:

λ pipenv run pytest -q -s --tb=no src/chapter-4/test_smtpsimple.py::test_ehlo_yield
F關(guān)閉SMTP連接

1 failed in 0.18s

我們可以看到在test_ehlo_yield執(zhí)行完后,又執(zhí)行了yield后面的代碼懂昂;

7.2. 使用with寫法

對于支持with寫法的對象介时,我們也可以隱式的執(zhí)行它的清理操作;

例如凌彬,上面的smtp_connection_yield也可以這樣寫:

@pytest.fixture()
def smtp_connection_yield():
    with smtplib.SMTP("smtp.163.com", 25, timeout=5) as smtp_connection:
        yield smtp_connection

7.3. 使用addfinalizer方法

fixture函數(shù)能夠接收一個request的參數(shù)沸柔,表示測試請求的上下文;我們可以使用request.addfinalizer方法為fixture添加清理函數(shù);

例如铲敛,上面的smtp_connection_yield也可以這樣寫:

@pytest.fixture()
def smtp_connection_fin(request):
    smtp_connection = smtplib.SMTP("smtp.163.com", 25, timeout=5)

    def fin():
        smtp_connection.close()

    request.addfinalizer(fin)
    return smtp_connection

注意:

yield之前或者addfinalizer注冊之前代碼發(fā)生錯誤退出的褐澎,都不會再執(zhí)行后續(xù)的清理操作

8. fixture可以訪問測試請求的上下文

fixture函數(shù)可以接收一個request的參數(shù),表示測試用例伐蒋、類工三、模塊,甚至測試會話的上下文環(huán)境先鱼;

我們可以擴展上面的smtp_connection_yield徒蟆,讓其根據(jù)不同的測試模塊使用不同的服務(wù)器:

# src/chapter-4/conftest.py

@pytest.fixture(scope='module')
def smtp_connection_request(request):
    server, port = getattr(request.module, 'smtp_server', ("smtp.163.com", 25))
    with smtplib.SMTP(server, port, timeout=5) as smtp_connection:
        yield smtp_connection
        print("斷開 %s:%d" % (server, port))

在測試模塊中指定smtp_server

# src/chapter-4/test_request.py

smtp_server = ("mail.python.org", 587)


def test_163(smtp_connection_request):
    response, _ = smtp_connection_request.ehlo()
    assert response == 250

我們來看看效果:

λ pipenv run pytest -q -s src/chapter-4/test_request.py
.斷開 mail.python.org:587

1 passed in 4.03s

9. fixture返回工廠函數(shù)

如果你需要在一個測試用例中,多次使用同一個fixture實例型型,相對于直接返回數(shù)據(jù)段审,更好的方法是返回一個產(chǎn)生數(shù)據(jù)的工廠函數(shù);

并且闹蒜,對于工廠函數(shù)產(chǎn)生的數(shù)據(jù)寺枉,也可以在fixture中對其管理:

@pytest.fixture
def make_customer_record():

    # 記錄生產(chǎn)的數(shù)據(jù)
    created_records = []

    # 工廠
    def _make_customer_record(name):
        record = models.Customer(name=name, orders=[])
        created_records.append(record)
        return record

    yield _make_customer_record

    # 銷毀數(shù)據(jù)
    for record in created_records:
        record.destroy()


def test_customer_records(make_customer_record):
    customer_1 = make_customer_record("Lisa")
    customer_2 = make_customer_record("Mike")
    customer_3 = make_customer_record("Meredith")

10. fixture的參數(shù)化

如果你需要在一系列的測試用例的執(zhí)行中,每輪執(zhí)行都使用同一個fixture绷落,但是有不同的依賴場景姥闪,那么可以考慮對fixture進行參數(shù)化;這種方式適用于對多場景的功能模塊進行詳盡的測試砌烁;

在之前的章節(jié)fixture可以訪問測試請求的上下文中筐喳,我們在測試模塊中指定不同smtp_server催式,得到不同的smtp_connection實例;

現(xiàn)在避归,我們可以通過指定params關(guān)鍵字參數(shù)創(chuàng)建兩個fixture實例荣月,每個實例供一輪測試使用,所有的測試用例執(zhí)行兩遍梳毙;在fixture的聲明函數(shù)中哺窄,可以使用request.param獲取當前使用的入?yún)ⅲ?/p>

# src/chapter-4/test_request.py

@pytest.fixture(scope='module', params=['smtp.163.com', "mail.python.org"])
def smtp_connection_params(request):
    server = request.param
    with smtplib.SMTP(server, 587, timeout=5) as smtp_connection:
        yield smtp_connection

在測試用例中使用這個fixture

# src/chapter-4/test_params.py

def test_parames(smtp_connection_params):
    response, _ = smtp_connection_params.ehlo()
    assert response == 250

執(zhí)行:

$ pipenv run pytest -q -s src/chapter-4/test_params.py 
.斷開 smtp.163.com:25
.斷開 smtp.126.com:25

2 passed in 0.26s

可以看到:

  • 這個測試用例使用不同的SMTP服務(wù)器,執(zhí)行了兩次账锹;

在參數(shù)化的fixture中萌业,pytest為每個fixture實例自動指定一個測試ID,例如:上述示例中的test_parames[smtp.163.com]test_parames[smtp.126.com]奸柬;

使用-k選項執(zhí)行一個指定的用例:

$ pipenv run pytest -q -s -k 163 src/chapter-4/test_params.py 
.斷開 smtp.163.com:25

1 passed, 1 deselected in 0.16s

使用--collect-only可以顯示這些測試ID生年,而不執(zhí)行用例:

$ pipenv run pytest -q -s --collect-only src/chapter-4/test_params.py 
src/chapter-4/test_params.py::test_parames[smtp.163.com]
src/chapter-4/test_params.py::test_parames[smtp.126.com]

no tests ran in 0.01s

同時,也可以使用ids關(guān)鍵字參數(shù)廓奕,自定義測試ID

# src/chapter-4/test_ids.py

@pytest.fixture(params=[0, 1], ids=['spam', 'ham'])
def a(request):
    return request.param


def test_a(a):
    pass

執(zhí)行--collect-only

$ pipenv run pytest -q -s --collect-only src/chapter-4/test_ids.py::test_a 
src/chapter-4/test_ids.py::test_a[spam]
src/chapter-4/test_ids.py::test_a[ham]

no tests ran in 0.01s

我們看到晶框,測試ID為我們指定的值;

數(shù)字懂从、字符串授段、布爾值和None在測試ID中使用的是它們的字符串表示形式:

# src/chapter-4/test_ids.py

def idfn(fixture_value):
    if fixture_value == 0:
        return "eggs"
    elif fixture_value == 1:
        return False
    elif fixture_value == 2:
        return None
    else:
        return fixture_value


@pytest.fixture(params=[0, 1, 2, 3], ids=idfn)
def b(request):
    return request.param


def test_b(b):
    pass

執(zhí)行--collect-only

$ pipenv run pytest -q -s --collect-only src/chapter-4/test_ids.py::test_b 
src/chapter-4/test_ids.py::test_b[eggs]
src/chapter-4/test_ids.py::test_b[False]
src/chapter-4/test_ids.py::test_b[2]
src/chapter-4/test_ids.py::test_b[3]

no tests ran in 0.01s

可以看到:

  • ids可以接收一個函數(shù),用于生成測試ID番甩;
  • 測試ID指定為None時侵贵,使用的是params原先對應(yīng)的值;

注意:

當測試params中包含元組缘薛、字典或者對象時窍育,測試ID使用的是fixture函數(shù)名+param的下標:

# src/chapter-4/test_ids.py

class C:
    pass


@pytest.fixture(params=[(1, 2), {'d': 1}, C()])
def c(request):
    return request.param


def test_c(c):
    pass

執(zhí)行--collect-only

$ pipenv run pytest -q -s --collect-only src/chapter-4/test_ids.py::test_c
src/chapter-4/test_ids.py::test_c[c0]
src/chapter-4/test_ids.py::test_c[c1]
src/chapter-4/test_ids.py::test_c[c2]

no tests ran in 0.01s

可以看到,測試IDfixture的函數(shù)名(c)加上對應(yīng)param的下標(從0開始)宴胧;

如果你不想這樣漱抓,可以使用str()方法或者復(fù)寫__str__()方法;

11. 在參數(shù)化的fixture中標記用例

fixtureparams參數(shù)中恕齐,可以使用pytest.param標記這一輪的所有用例乞娄,其用法和在pytest.mark.parametrize中的用法一樣;

# src/chapter-4/test_fixture_marks.py

import pytest


@pytest.fixture(params=[('3+5', 8),
                        pytest.param(('6*9', 42),
                                     marks=pytest.mark.xfail,
                                     id='failed')])
def data_set(request):
    return request.param


def test_data(data_set):
    assert eval(data_set[0]) == data_set[1]

我們使用pytest.param(('6*9', 42), marks=pytest.mark.xfail, id='failed')的形式指定一個request.param入?yún)⑾云纾渲?code>marks表示當用例使用這個入?yún)r仪或,跳過執(zhí)行將用例標記為xfail;并且士骤,我們還使用id為此時的用例指定了一個測試ID范删;

$ pipenv run pytest -v src/chapter-4/test_fixture_marks.py::test_data
============================ test session starts ============================
platform darwin -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0 -- /Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-EK3zIUmM/bin/python3.7
cachedir: .pytest_cache
rootdir: /Users/yaomeng/Private/Projects/pytest-chinese-doc
collected 2 items                                                           

src/chapter-4/test_fixture_marks.py::test_data[data_set0] PASSED      [ 50%]
src/chapter-4/test_fixture_marks.py::test_data[failed] XFAIL          [100%]

======================= 1 passed, 1 xfailed in 0.08s ========================

可以看到:

  • 用例結(jié)果是XFAIL,而不是FAILED拷肌;
  • 測試ID是我們指定的failed到旦,而不是data_set1旨巷;

我們也可以使用pytest.mark.parametrize實現(xiàn)相同的效果:

# src/chapter-4/test_fixture_marks.py

@pytest.mark.parametrize(
    'test_input, expected',
    [('3+5', 8),
     pytest.param('6*9', 42, marks=pytest.mark.xfail, id='failed')])
def test_data2(test_input, expected):
    assert eval(test_input) == expected

執(zhí)行:

pipenv run pytest -v src/chapter-4/test_fixture_marks.py::test_data2
============================ test session starts ============================
platform darwin -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0 -- /Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-EK3zIUmM/bin/python3.7
cachedir: .pytest_cache
rootdir: /Users/yaomeng/Private/Projects/pytest-chinese-doc
collected 2 items                                                           

src/chapter-4/test_fixture_marks.py::test_data2[3+5-8] PASSED         [ 50%]
src/chapter-4/test_fixture_marks.py::test_data2[failed] XFAIL         [100%]

======================= 1 passed, 1 xfailed in 0.07s ========================

12. 模塊化:fixture使用其它的fixture

你不僅僅可以在測試用例上使用fixture,還可以在fixture的聲明函數(shù)中使用其它的fixture添忘;這有助于模塊化的設(shè)計你的fixture采呐,可以在多個項目中重復(fù)使用框架級別的fixture

一個簡單的例子昔汉,我們可以擴展之前src/chapter-4/test_params.py的例子,實例一個app對象:

# src/chapter-4/test_appsetup.py

import pytest


class App:
    def __init__(self, smtp_connection):
        self.smtp_connection = smtp_connection


@pytest.fixture(scope='module')
def app(smtp_connection_params):
    return App(smtp_connection_params)


def test_smtp_connection_exists(app):
    assert app.smtp_connection

我們創(chuàng)建一個fixture app并調(diào)用之前在conftest.py中定義的smtp_connection_params拴清,返回一個App的實例靶病;

執(zhí)行:

$ pipenv run pytest -v src/chapter-4/test_appsetup.py 
============================ test session starts ============================
platform darwin -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0 -- /Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-EK3zIUmM/bin/python3.7
cachedir: .pytest_cache
rootdir: /Users/yaomeng/Private/Projects/pytest-chinese-doc
collected 2 items                                                           

src/chapter-4/test_appsetup.py::test_smtp_connection_exists[smtp.163.com] PASSED [ 50%]
src/chapter-4/test_appsetup.py::test_smtp_connection_exists[smtp.126.com] PASSED [100%]

============================= 2 passed in 1.25s =============================

因為app使用了參數(shù)化的smtp_connection_params,所以測試用例test_smtp_connection_exists會使用不同的App實例執(zhí)行兩次口予,并且娄周,app并不需要關(guān)心smtp_connection_params的實現(xiàn)細節(jié);

app的作用域是模塊級別的沪停,它又調(diào)用了smtp_connection_params煤辨,也是模塊級別的,如果smtp_connection_params會話級別的作用域木张,這個例子還是一樣可以正常工作的众辨;這是因為低級別的作用域可以調(diào)用高級別的作用域,但是高級別的作用域調(diào)用低級別的作用域會返回一個ScopeMismatch的異常舷礼;

13. 高效的利用fixture實例

在測試期間鹃彻,pytest只激活最少個數(shù)的fixture實例;如果你擁有一個參數(shù)化的fixture妻献,所有使用它的用例會在創(chuàng)建的第一個fixture實例并銷毀后蛛株,才會去使用第二個實例;

下面這個例子育拨,使用了兩個參數(shù)化的fixture谨履,其中一個是模塊級別的作用域,另一個是用例級別的作用域熬丧,并且使用print方法打印出它們的setup/teardown流程:

# src/chapter-4/test_minfixture.py

import pytest


@pytest.fixture(scope="module", params=["mod1", "mod2"])
def modarg(request):
    param = request.param
    print("  SETUP modarg", param)
    yield param
    print("  TEARDOWN modarg", param)


@pytest.fixture(scope="function", params=[1, 2])
def otherarg(request):
    param = request.param
    print("  SETUP otherarg", param)
    yield param
    print("  TEARDOWN otherarg", param)


def test_0(otherarg):
    print("  RUN test0 with otherarg", otherarg)


def test_1(modarg):
    print("  RUN test1 with modarg", modarg)


def test_2(otherarg, modarg):
    print("  RUN test2 with otherarg {} and modarg {}".format(otherarg, modarg))

執(zhí)行:

$ pipenv run pytest -q -s src/chapter-4/test_minfixture.py 
  SETUP otherarg 1
  RUN test0 with otherarg 1
.  TEARDOWN otherarg 1
  SETUP otherarg 2
  RUN test0 with otherarg 2
.  TEARDOWN otherarg 2
  SETUP modarg mod1
  RUN test1 with modarg mod1
.  SETUP otherarg 1
  RUN test2 with otherarg 1 and modarg mod1
.  TEARDOWN otherarg 1
  SETUP otherarg 2
  RUN test2 with otherarg 2 and modarg mod1
.  TEARDOWN otherarg 2
  TEARDOWN modarg mod1
  SETUP modarg mod2
  RUN test1 with modarg mod2
.  SETUP otherarg 1
  RUN test2 with otherarg 1 and modarg mod2
.  TEARDOWN otherarg 1
  SETUP otherarg 2
  RUN test2 with otherarg 2 and modarg mod2
.  TEARDOWN otherarg 2
  TEARDOWN modarg mod2

8 passed in 0.02s

可以看出:

  • mod1TEARDOWN操作完成后笋粟,才開始mod2SETUP操作;
  • 用例test_0獨立完成測試析蝴;
  • 用例test_1test_2都使用到了模塊級別的modarg矗钟,同時test_2也使用到了用例級別的otherarg。它們執(zhí)行的順序是嫌变,test_1先使用mod1吨艇,接著test_2使用mod1otherarg 1/otherarg 2,然后test_1使用mod2腾啥,最后test_2使用mod2otherarg 1/otherarg 2东涡;也就是說test_1test_2共用相同的modarg實例谅年,最少化的保留fixture的實例個數(shù);

14. 在類挂据、模塊和項目級別上使用fixture實例

有時丸边,我們并不需要在測試用例中直接使用fixture實例;例如祖娘,我們需要一個空的目錄作為當前用例的工作目錄失尖,但是我們并不關(guān)心如何創(chuàng)建這個空目錄;這里我們可以使用標準的tempfile模塊來實現(xiàn)這個功能渐苏;

# src/chapter-4/conftest.py

import pytest
import tempfile
import os


@pytest.fixture()
def cleandir():
    newpath = tempfile.mkdtemp()
    os.chdir(newpath)

在測試中使用usefixtures標記聲明使用它:

# src/chapter-4/test_setenv.py

import os
import pytest


@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit:
    def test_cwd_starts_empty(self):
        assert os.listdir(os.getcwd()) == []
        with open("myfile", "w") as f:
            f.write("hello")

    def test_cwd_again_starts_empty(self):
        assert os.listdir(os.getcwd()) == []

得益于usefixtures標記掀潮,測試類TestDirectoryInit中所有的測試用例都可以使用cleandir,這和在每個測試用例中指定cleandir參數(shù)是一樣的琼富;

執(zhí)行:

$ pipenv run pytest -q -s src/chapter-4/test_setenv.py 
..
2 passed in 0.02s

你可以使用如下方式指定多個fixture

@pytest.mark.usefixtures("cleandir", "anotherfixture")
def test():
    ...

你也可以使用如下方式為測試模塊指定fixture

pytestmark = pytest.mark.usefixtures("cleandir")

注意:參數(shù)的名字必須pytestmark;

你也可以使用如下方式為整個項目指定fixture

# src/chapter-4/pytest.ini

[pytest]
usefixtures = cleandir

注意:

usefixtures標記不適用于fixture聲明函數(shù)仪吧;例如:

@pytest.mark.usefixtures("my_other_fixture")
@pytest.fixture
def my_fixture_that_sadly_wont_use_my_other_fixture():
  ...

這并不會返回任何的錯誤或告警,具體討論可以參考#3664

15. 自動使用fixture

有時候鞠眉,你想在測試用例中自動使用fixture薯鼠,而不是作為參數(shù)使用或者usefixtures標記;設(shè)想械蹋,我們有一個數(shù)據(jù)庫相關(guān)的fixture出皇,包含begin/rollback/commit的體系結(jié)構(gòu),現(xiàn)在我們希望通過begin/rollback包裹每個測試用例哗戈;

下面恶迈,通過列表實現(xiàn)一個虛擬的例子:

# src/chapter-4/test_db_transact.py

import pytest


class DB:
    def __init__(self):
        self.intransaction = []

    def begin(self, name):
        self.intransaction.append(name)

    def rollback(self):
        self.intransaction.pop()


@pytest.fixture(scope="module")
def db():
    return DB()


class TestClass:
    @pytest.fixture(autouse=True)
    def transact(self, request, db):
        db.begin(request.function.__name__)
        yield
        db.rollback()

    def test_method1(self, db):
        assert db.intransaction == ["test_method1"]

    def test_method2(self, db):
        assert db.intransaction == ["test_method2"]

類級別作用域transact函數(shù)中聲明了autouse=True,所以TestClass中的所有用例谱醇,可以自動調(diào)用transact而不用顯式的聲明或標記暇仲;

執(zhí)行:

$ pipenv run pytest -q -s src/chapter-4/test_db_transact.py 
..
2 passed in 0.01s

autouse=Truefixture在其它級別作用域中的工作流程:

  • autouse fixture遵循scope關(guān)鍵字的定義:如果其含有scope='session',則不管它在哪里定義的副渴,都將只執(zhí)行一次奈附;scope='class'表示每個測試類執(zhí)行一次;
  • 如果在測試模塊中定義autouse fixture煮剧,那么這個測試模塊所有的用例自動使用它斥滤;
  • 如果在conftest.py中定義autouse fixture,那么它的相同文件夾和子文件夾中的所有測試模塊中的用例都將自動使用它勉盅;
  • 如果在插件中定義autouse fixture佑颇,那么所有安裝這個插件的項目中的所有用例都將自動使用它;

上述的示例中草娜,我們期望只有TestClass的用例自動調(diào)用fixture transact挑胸,這樣我們就不希望transact一直處于激活的狀態(tài),所以更標準的做法是宰闰,將transact聲明在conftest.py中茬贵,而不是使用autouse=True

@pytest.fixture
def transact(request, db):
    db.begin()
    yield
    db.rollback()

并且簿透,在TestClass上聲明:

@pytest.mark.usefixtures("transact")
class TestClass:
    def test_method1(self):
        ...

其它類或者用例也想使用的話,同樣需要顯式的聲明usefixtures解藻;

16. 在不同的層級上覆寫fixture

在大型的測試中老充,你可能需要在本地覆蓋項目級別的fixture,以增加可讀性和便于維護螟左;

16.1. 在文件夾(conftest.py)層級覆寫fixture

假設(shè)我們有如下的測試項目:

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        import pytest

        @pytest.fixture
        def username():
            return 'username'

    test_something.py
        # content of tests/test_something.py
        def test_username(username):
            assert username == 'username'

    subfolder/
        __init__.py

        conftest.py
            # content of tests/subfolder/conftest.py
            import pytest

            @pytest.fixture
            def username(username):
                return 'overridden-' + username

        test_something.py
            # content of tests/subfolder/test_something.py
            def test_username(username):
                assert username == 'overridden-username'

可以看到:

  • 子文件夾conftest.py中的fixture覆蓋了上層文件夾中同名的fixture啡浊;
  • 子文件夾conftest.py中的fixture可以輕松的訪問上層文件夾中同名的fixture

16.2. 在模塊層級覆寫fixture

假設(shè)我們有如下的測試項目:

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        import pytest

        @pytest.fixture
        def username():
            return 'username'

    test_something.py
        # content of tests/test_something.py
        import pytest

        @pytest.fixture
        def username(username):
            return 'overridden-' + username

        def test_username(username):
            assert username == 'overridden-username'

    test_something_else.py
        # content of tests/test_something_else.py
        import pytest

        @pytest.fixture
        def username(username):
            return 'overridden-else-' + username

        def test_username(username):
            assert username == 'overridden-else-username'

可以看到:

  • 模塊中的fixture覆蓋了conftest.py中同名的fixture胶背;
  • 模塊中的fixture可以輕松的訪問conftest.py中同名的fixture巷嚣;

16.3. 在用例參數(shù)中覆寫fixture

假設(shè)我們有如下的測試項目:

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        import pytest

        @pytest.fixture
        def username():
            return 'username'

        @pytest.fixture
        def other_username(username):
            return 'other-' + username

    test_something.py
        # content of tests/test_something.py
        import pytest

        @pytest.mark.parametrize('username', ['directly-overridden-username'])
        def test_username(username):
            assert username == 'directly-overridden-username'

        @pytest.mark.parametrize('username', ['directly-overridden-username-other'])
        def test_username_other(other_username):
            assert other_username == 'other-directly-overridden-username-other'

可以看到:

  • fixture的值被用例的參數(shù)所覆蓋;
  • 盡管用例test_username_other沒有使用username奄妨,但是other_username使用到了username涂籽,所以也同樣受到了影響苹祟;

16.4. 參數(shù)化的fixture覆寫非參數(shù)化的fixture砸抛,反之亦然

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        import pytest

        @pytest.fixture(params=['one', 'two', 'three'])
        def parametrized_username(request):
            return request.param

        @pytest.fixture
        def non_parametrized_username(request):
            return 'username'

    test_something.py
        # content of tests/test_something.py
        import pytest

        @pytest.fixture
        def parametrized_username():
            return 'overridden-username'

        @pytest.fixture(params=['one', 'two', 'three'])
        def non_parametrized_username(request):
            return request.param

        def test_username(parametrized_username):
            assert parametrized_username == 'overridden-username'

        def test_parametrized_username(non_parametrized_username):
            assert non_parametrized_username in ['one', 'two', 'three']

    test_something_else.py
        # content of tests/test_something_else.py
        def test_username(parametrized_username):
            assert parametrized_username in ['one', 'two', 'three']

        def test_username(non_parametrized_username):
            assert non_parametrized_username == 'username'

可以看出:

  • 參數(shù)化的fixture和非參數(shù)化的fixture同樣可以相互覆蓋;
  • 在模塊層級上的覆蓋不會影響其它模塊树枫;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末直焙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子砂轻,更是在濱河造成了極大的恐慌奔誓,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搔涝,死亡現(xiàn)場離奇詭異厨喂,居然都是意外死亡,警方通過查閱死者的電腦和手機庄呈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門蜕煌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人诬留,你說我怎么就攤上這事斜纪。” “怎么了文兑?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵盒刚,是天一觀的道長。 經(jīng)常有香客問我绿贞,道長因块,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任籍铁,我火速辦了婚禮贮聂,結(jié)果婚禮上靠柑,老公的妹妹穿的比我還像新娘。我一直安慰自己吓懈,他們只是感情好歼冰,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著耻警,像睡著了一般隔嫡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上甘穿,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天腮恩,我揣著相機與錄音,去河邊找鬼温兼。 笑死秸滴,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的募判。 我是一名探鬼主播荡含,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼届垫!你這毒婦竟也來了释液?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤装处,失蹤者是張志新(化名)和其女友劉穎误债,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體妄迁,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡寝蹈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了登淘。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片箫老。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖形帮,靈堂內(nèi)的尸體忽然破棺而出槽惫,到底是詐尸還是另有隱情,我是刑警寧澤辩撑,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布界斜,位于F島的核電站,受9級特大地震影響合冀,放射性物質(zhì)發(fā)生泄漏各薇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望峭判。 院中可真熱鬧开缎,春花似錦、人聲如沸林螃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽疗认。三九已至完残,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間横漏,已是汗流浹背谨设。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留缎浇,地道東北人扎拣。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像素跺,于是被迫代替她去往敵國和親二蓝。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348