[跟着官方文档学pytest][六][fixture][学习笔记]

举报
John2021 发表于 2022/05/17 06:55:05 2022/05/17
【摘要】 1.teardown/cleanup(也叫fixture finalization)当我们运行我们的测试时,希望确保它们自己清理干净,这样它们就不会与任何其他测试相混淆(同时也不会留下大量的测试数据使得系统膨胀)。pytest中的fixtures提供了一个非常有用的teardown系统,允许我们定义每个fixtures自行清理的特定步骤。该系统可以通过两种方式加以利用。 1.1.yield...

1.teardown/cleanup(也叫fixture finalization)

当我们运行我们的测试时,希望确保它们自己清理干净,这样它们就不会与任何其他测试相混淆(同时也不会留下大量的测试数据使得系统膨胀)。pytest中的fixtures提供了一个非常有用的teardown系统,允许我们定义每个fixtures自行清理的特定步骤。
该系统可以通过两种方式加以利用。

1.1.yield fixtures(建议)

"Yield"fixtures yield而不是return。使用这些fixtures,我们可以运行一些代码并将对象传递回请求fixture/测试,就像其他fixtures一样。唯一的区别是:

  1. return换成yield。
  2. 该fixtures的任何teardown代码都放在yield之后。

一旦pytest计算出fixtures的线性顺序,它将运行每个fixture直到return或者yield,然后移动到列表中的下一个fixture做同样的事情。
测试完成后,pytest将返回fixture列表,但顺序相反,取出每个yielded,并在其中运行yield语句之后的代码。
以下是一个简单的电子邮件模块:

# content of emaillib.py
class MailAdminClient:
    def create_user(self):
        return MailUser()
    def delete_user(self):
        # do some cleanup
        pass
class MailUser:
    def __init__(self):
        self.inbox=[]
    def send_mail(self,email,other):
        other.inbox.append(email)
    def clear_mailbox(self):
        self.inbox.clear()
class Email:
    def __init__(self,subject,body):
        self.subject=subject
        self.body=body

假设我们要测试从一个用户向另一个用户发送电子邮件。 我们必须首先创建每个用户,然后将电子邮件从一个用户发送给另一个用户,最后断言另一个用户在他们的收件箱中收到了该邮件。 如果我们想在测试运行后进行清理,我们可能必须确保其他用户的邮箱在删除该用户之前清空,否则系统可能会报错。
这可能是这样的:

# content of test_emaillib.py
import pytest

from emaillib import Email, MailAdminClient


@pytest.fixture
def mail_admin():
    return MailAdminClient()


@pytest.fixture
def sending_user(mail_admin):
    user = mail_admin.create_user()
    yield user
    mail_admin.delete_user(user)


@pytest.fixture
def receiving_user(mail_admin):
    user = mail_admin.create_user()
    yield user
    mail_admin.delete_user(user)


def test_email_received(sending_user, receiving_user):
    email = Email(subject="Hey!", body="How's it going?")
    sending_user.send_email(email, receiving_user)
    assert email in receiving_user.inbox

因为receiving_user时设置时最后一个fixture,所以它是第一个运行teardown的。
有一种风险,即使在teardown有正确的顺序也不能保证安全清理。在safe teardown中有详细介绍。

1.1.1.Handling errors for yield fixture

如果yield fixture在yield之前引发了异常,pytest不会尝试在该yield fixture的yield语句之后运行teardown代码。但是,对于已经成功运行该测试的每个fixture,pytest仍将尝试teardown。

1.2.Adding finalizers directly

虽然yield fixture被认为是更清晰、更直接的选择,但还有另一种选择,即直接将"finalizer"函数添加到测试的request-context对象中。它带来了与yield fixtures类似的结果,但更冗长。
为了使用这种方法,我们必须在需要为其添加teardown代码的fixture中请求request-context对象(就像我们请求另一个fixture一样),然后将包含该teardown代码的可调用代码传递给其addfinalizer方法。
但我们要小心,因为pytest将在添加该finalizer后运行,即使fixture在添加finalizer后引发异常。因此,为了确保我们不会在不需要的时候运行finalizer,我们只会在fixture完成我们需要的teardown后添加finalizer。
下面是使用addfinalizer方法运行上一个示例:

# content of test_emaillib.py
import pytest

from emaillib import Email, MailAdminClient


@pytest.fixture
def mail_admin():
    return MailAdminClient()


@pytest.fixture
def sending_user(mail_admin):
    user = mail_admin.create_user()
    yield user
    mail_admin.delete_user(user)


@pytest.fixture
def receiving_user(mail_admin, request):
    user = mail_admin.create_user()

    def delete_user():
        mail_admin.delete_user(user)

    request.addfinalizer(delete_user)
    return user


@pytest.fixture
def email(sending_user, receiving_user, request):
    _email = Email(subject="Hey!", body="How's it going?")
    sending_user.send_email(_email, receiving_user)

    def empty_mailbox():
        receiving_user.clear_mailbox()

    request.addfinalizer(empty_mailbox)
    return _email


def test_email_received(receiving_user, email):
    assert email in receiving_user.inbox

2.safe teardowns

pytest的fixture系统非常强大,但仍然由计算机运行,因此它无法弄清楚如何安全进行teardown。如果我们不小心,错误位置的错误可能会给我们导致进一步的错误。
例如,考虑一下测试(基于上面的邮件示例):

# content of test_emaillib.py
import pytest

from emaillib import Email, MailAdminClient


@pytest.fixture
def setup():
    mail_admin = MailAdminClient()
    sending_user = mail_admin.create_user()
    receiving_user = mail_admin.create_user()
    email = Email(subject="Hey!", body="How's it going?")
    sending_user.send_email(email, receiving_user)
    yield receiving_user, email
    receiving_user.clear_mailbox()
    mail_admin.delete_user(sending_user)
    mail_admin.delete_user(receiving_user)


def test_email_received(setup):
    receiving_user, email = setup
    assert email in receiving_user.inbox

这个版本更紧凑,也更难阅读,没有一个描述性很强的fixture名称,而且没有一个fixture可以重用。
还有一个更严重的问题,如果设置中任何步骤发生异常,则任何teardown代码都不会运行。
还有一个更严重的问题,如果设置中的任何步骤引发异常,则任何teardown都不会运行。
一种选择可能是使用addfinalizer方法而不是yield,但这可能会变得非常复杂且难以维护(而且不再紧凑)。

2.1.safe fixture structure

如上面的电子邮件示例所示,最安全和最简单的fixture结构要求将fixture限制为每个仅执行一个状态更改操作,然后将它们与其teardown代码捆绑在一起。
状态更改操作可能失败但仍然修改状态的可能性可以忽略不计,因为这些操作中的大多数往往是基于事务的(至少在状态可能被抛在后面的测试级别上)。因此,如果我们通过将任何成功的状态更改操作移动到单独的fixture函数并将其与其他可能失败的状态更改操作分开来确保任何成功的状态更改操作都被teardown。
例如,假设我们有一个带有登录页面的网站,并且我们可以访问一个可以生成用户的管理API。对于我们的测试,我们希望:

  1. 通过admin的API创建用户
  2. 使用Selenium启动浏览器
  3. 访问login页面
  4. 使用我们创建的用户登录
  5. 在登陆页面显示他们的名字

我们不想让用户留在系统,也不想浏览器会话继续运行,所以我们要确保创建这些东西的fixtures自己清理干净。
注意:对于这个示例,某些fixtures(比如base_url和admin_credentials)暗示存在于其他地方。所以现在,我们假设它们存在,我们只是不看它们。

from uuid import uuid4
from urllib.parse import urljoin

from selenium.webdriver import Chrome
import pytest

from src.utils.pages import LoginPage,LandingPage
from src.utils import AdminApiClient
from src.utils.data_types import User

@pytest.fixture
def admin_client(base_url,admin_credentials):
    return AdminApiClient(base_url,**admin_credentials)
@pytest.fixture
def user(admin_client):
    _user=User(name="Susan",username=f"testuser-{uuid4()}",password="P4$$word")
    admin_client.create_user(_user)
    yield _user
    admin_client.delete_user(_user)
@pytest.fixture
def driver():
    _driver=Chrome()
    yield _driver
    _driver.quit()
@pytest.fixture
def login(driver,base_url,user):
    driver.get(urljoin(base_url,"/login"))
    page=LoginPage(driver)
    page.login(user)
@pytest.fixture
def landing_page(driver,login):
    return LandingPage(driver)
def test_name_on_landing_page_after_login(landing_page,user):
    assert landing_page.header==f"Welcome,{user.name}!"

依赖项的布局方式意味着尚不清楚用户fixture是否会在驱动程序fixture之前执行。但这没关系,因为这些都是原子操作,所以哪个先运行并不重要,因为测试的事件序列仍然是线性的。但重要的是,无论哪一个先运行,如果一个提出异常而另一个不会,那么两者都不会留下任何东西。如果驱动程序在用户之前执行,并且用户引发异常,则驱动程序仍将退出,并且从未创建用户。如果驱动程序是引发异常的驱动程序,则永远不会启动驱动程序,也永远不会创建用户。

3.Running multiple assert statements safely

有时可能希望在完成所有这些设置后运行多个断言,这是有道理的,因为在更复杂的系统中,单个操作可以启动多个行为。pytest有一种方便的方法来处理这个问题,它结合了我们迄今为止已经讨论过的一堆内容。
所需要的只是升级到更大的范围,然后将act步骤定义为autouse fixture,最后,确保所有fixtures都针对更高级别的范围。
以上一个示例为例,并对其进行一些调整。假设除了检查header中的欢迎消息外,我们还希望检查注销按钮以及指向用户配置文件的链接。

# contents of tests/end_to_end/test_login.py
from uuid import uuid4
from urllib.parse import urljoin

from selenium.webdriver import Chrome
import pytest

from src.utils.pages import LoginPage, LandingPage
from src.utils import AdminApiClient
from src.utils.data_types import User


@pytest.fixture(scope="class")
def admin_client(base_url, admin_credentials):
    return AdminApiClient(base_url, **admin_credentials)


@pytest.fixture(scope="class")
def user(admin_client):
    _user = User(name="Susan", username=f"testuser-{uuid4()}", password="P4$$word")
    admin_client.create_user(_user)
    yield _user
    admin_client.delete_user(_user)


@pytest.fixture(scope="class")
def driver():
    _driver = Chrome()
    yield _driver
    _driver.quit()


@pytest.fixture(scope="class")
def landing_page(driver, login):
    return LandingPage(driver)


class TestLandingPageSuccess:
    @pytest.fixture(scope="class", autouse=True)
    def login(self, driver, base_url, user):
        driver.get(urljoin(base_url, "/login"))
        page = LoginPage(driver)
        page.login(user)

    def test_name_in_header(self, landing_page, user):
        assert landing_page.header == f"Welcome, {user.name}!"

    def test_sign_out_button(self, landing_page):
        assert landing_page.sign_out_button.is_displayed()

    def test_profile_link(self, landing_page, user):
        profile_href = urljoin(base_url, f"/profile?id={user.profile_id}")
        assert landing_page.profile_link.get_attribute("href") == profile_href

注意这些方法只是在签名中引用self作为一种形式。没有状态与实际测试类相关联,因为它可能在unittest.TestCase框架中。一切都由pytest fixture系统管理。
每个方法只需要请求它实际需要的fixtures,而不用担心顺序。这是因为act fixture是一个autouse fixture,它确保所有其他fixture在它之前执行。不再需要进行状态更改,因此测试可以根据需要自由地进行尽可能多的非状态更改查询,而不会冒踩到其他测试的风险。
登录fixture也在类内部定义,因为并非模块中的其他测试都期望成功登录,并且对于另一个测试类可能稍微不同地处理该行为。例如,如果我们想围绕提交错误编写另一个测试场景,可以通过在测试文件中添加类似这样的内容来处理:

class TestLandingPageBadCredentials:
    @pytest.fixture(scope="class")
    def faux_user(self, user):
        _user = deepcopy(user)
        _user.password = "badpass"
        return _user

    def test_raises_bad_credentials_exception(self, login_page, faux_user):
        with pytest.raises(BadCredentialsException):
            login_page.login(faux_user)

4.Fixtures can introspect the requeting test context

Fixture函数可以接受请求对象来内省"requesting"测试函数、类或模块上下文。进一步扩展之前的 smtp_connection示例,让我们从使用我们的fixture的测试模块中读取一个可选的服务器URL:

# content of conftest.py
import pytest
import smtplib

@pytest.fixture(scope="module")
def smtp_connection(request):
    server=getattr(request.module,"smtpserver","smtp.qq.com")
    smtp_connection=smtplib.SMTP(server,465,timeout=5)
    yield smtp_connection
    print("finalizing {} ({})".format(smtp_connection,server))
    smtp_connection.close()

我们使用request.module属性来选择性从测试模块获取smtpserver属性。

5.Using markers to pass data to fixtures

使用request对象,fixture还可以访问用于测试函数的标记。这对于将数据从测试传递到fixture中非常有用。

import pytest
@pytest.fixture
def fixt(request):
    marker=request.node.get_closet_marker("fixt_data")
    if marker is None:
        # Handle missing marker is some way...
        data=None
    else:
        data=marker.args[0]
    # Do something with the data
    return data
@pytest.mark.fixt_data(42)
def test_fixt(fixt):
    assert fixt==42

6.Factories as fixtures

"工厂作为fixture"模式可以在单个测试中多次需要fixture结果的情况下提供帮助。fixture不是直接返回数据,而是返回一个生成数据的函数。然后,可以在测试中多次调用此函数。

import pytest
@pytest.fixture
def make_customer_record():
    def _make_customer_record(name):
        return {"name":name,"orders":[]}
    return _make_customer_record
def test_customer_records(make_customer_record):
    customer1=make_customer_record("Lisa")
    customer2=make_customer_record("Mike")
    customer3=make_customer_record("Meredith")
    print("\n")
    print(customer1)#{'name': 'Lisa', 'orders': []}
    print(customer2)#{'name': 'Mike', 'orders': []}
    print(customer3)#{'name': 'Meredith', 'orders': []}

7.Using marks with parametrized fixtures

pytest.param()可用于在参数化fixture的集合中,就像它们可以与@pytest.mark.parametrize一起使用一样

import pytest
@pytest.fixture(params=[0,1,pytest.param(2,marks=pytest.mark.skip)])
def data_set(request):
    return request.param
def test_data(data_set):
    pass

运行测试将跳过对值为2的data_set的调用

============================= test session starts =============================
collecting ... collected 3 items

test_fixture_marks.py::test_data[0] PASSED                               [ 33%]
test_fixture_marks.py::test_data[1] PASSED                               [ 66%]
test_fixture_marks.py::test_data[2] SKIPPED (unconditional skip)         [100%]
Skipped: unconditional skip
======================== 2 passed, 1 skipped in 0.01s =========================
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。