pytest学习总结2.3 - 如何使用固定装置 fixtures (1)

2.3 如何使用固定装置 fixtures

2.3.1 请求并调用固定装置 fixture

  1. 在基本级别上,测试函数通过声明它们为参数来请求它们所需要的固定装置。
    当pytest去运行测试,它会查看该函数的签名参数,然后搜索与这些参数的名称相同的固定装置。一旦pytest找到了它们,它就会运行这些固定装置,捕获它们返回的内容(如果有的话),并将这些对象作为参数传递给测试函数。
import pytest
class Fruit:
    def __init__(self, name):
        self.name = name
        self.cubed = False

    def cube(self):
        self.cubed = True
class FruitSalad:
    def __init__(self, *fruit_bowl):
        self.fruit = fruit_bowl
        self._cube_fruit()

    def _cube_fruit(self):
        for fruit in self.fruit:
            fruit.cube()

@pytest.fixture
def fruit_bowl():
    return [Fruit("apple"), Fruit("banana")]

def test_fruit_salad(fruit_bowl):
    # Act
    fruit_salad = FruitSalad(*fruit_bowl)
    # Assert
    for fruit in fruit_salad.fruit:
        print(fruit.cubed)
    assert all(fruit.cubed for fruit in fruit_salad.fruit)

不适用固定装置来实现:

import pytest
class Fruit:
    def __init__(self, name):
        self.name = name
        self.cubed = False

    def cube(self):
        self.cubed = True
class FruitSalad:
    def __init__(self, *fruit_bowl):
        self.fruit = fruit_bowl
        self._cube_fruit()

    def _cube_fruit(self):
        for fruit in self.fruit:
            fruit.cube()
            
def fruit_bowl():
    return [Fruit("apple"), Fruit("banana")]
def test_fruit_salad(fruit_bowl):
    # Act
    fruit_salad = FruitSalad(*fruit_bowl)
    # Assert
    assert all(fruit.cubed for fruit in fruit_salad.fruit)
# Arrange
bowl = fruit_bowl()
test_fruit_salad(fruit_bowl=bowl)
  1. 固定装置可以请求调用其他固定装置
    pytest最大的优势之一就是灵活的夹具系统,它允许我们将测试的复杂需求归结为更简单和有组织的功能,我们只需要让每个人来描述它们所依赖的东西。示例:
import pytest

# 布置一个夹具
@pytest.fixture
def first_entry():
    return "a"

# 布置第二个夹具
@pytest.fixture
def order(first_entry):
    return [first_entry]

# 调用第二个夹具
def test_string(order):
    # Act
    order.append("b")
    # Assert
    assert order == ["a", "b"]

def test_int(order):
    # Act
    order.append(2)
    # Assert
    assert order == ["a", 2]
  1. 测试用例或者固定装置fixture可以请求多个固定装置 fixture
    测试和装置并不限于一次要求一个装置。他们可以要求多少就要求多少
import pytest

@pytest.fixture
def first_entry():
    return "a"

@pytest.fixture
def second_entry():
    return 2

@pytest.fixture
def order(first_entry, second_entry):
    return [first_entry, second_entry]

@pytest.fixture
def expected_list():
    return ["a", 2, 3.0]

def test_int(order, expected_list):
    # Act
    order.append(3.0)
    # Assert
    assert order == expected_list
  1. 每次测试可以多次请求固定装置(返回值会被缓存)
    在同一测试中也可以多次请求固定装置,并且pytest不会在该测试中再次执行它们,这些装置不会被执行第二次。
import pytest

# Arrange
@pytest.fixture
def first_entry():
    return "a"

# Arrange
@pytest.fixture
def order():
    return []

# Act
@pytest.fixture
def append_first(order, first_entry):
    return order.append(first_entry)

def test_string_only(append_first, order, first_entry):
    # Assert
    print(append_first)  # None
    print(order)  # ['a'] 这里的order有a,是因为调用了order.append(first_entry),夹具 order 的新值被缓存了
    assert order == [first_entry]

2.3.2 自动固定装置(不需要你请求调用)

有时你可能想要一个固定装置(甚至几个),这些装置是被所有的测试环境依赖,“Autouse”的固定装置是一种方便的方式,使所有的测试自动请求他们。这可以减少大量的冗余请求,甚至可以提供更高级的使用固定装置,
在本例中,append_first固定装置是一种自动固定装置。因为它是自动发生的,所以两个测试都会受到它的影响,即使两个测试都没有要求它。但这并不意味着他们不能被要求;只是这是没有必要的。

import pytest

@pytest.fixture
def first_entry():
    return "a"

@pytest.fixture
def order():
    return []

# Act
@pytest.fixture(autouse=True)
def append_first(order, first_entry):
    return order.append(first_entry)

def test_string_only(order, first_entry):
    assert order == [first_entry]

def test_string_and_int(order, first_entry):
    order.append(2)
    assert order == [first_entry, 2]
collected 2 items                                                                                                                                                                                                                         
test_append.py::test_string_only PASSED
test_append.py::test_string_and_int PASSED

2.3.3 固定装置的范围:function, class, module, package or session.

需要网络访问的设备依赖于连接,创建通常花费昂贵。扩展前面的示例,我们可以向@pytest添加一个scope=“模块”参数。夹具调用导致smtp_connection夹具函数,
负责创建到已存在的SMTP服务器的连接,每个测试模块只调用一次(默认是每个测试函数调用一次)。
因此,一个测试模块中的多个测试功能将每个接收相同的smtp_connection夹具实例,从而节省时间。作用域的可能值包括:函数、类、模块、程序包、会话。

# content of conftest.py
import pytest
import smtplib
@pytest.fixture(scope="module")
def smtp_connection():
    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
#  test_module.py
def test_ehlo(smtp_connection):
    response, msg = smtp_connection.ehlo()
    assert response == 250
    assert b"smtp.gmail.com" in msg
    assert 0 # for demo purposes
def test_noop(smtp_connection):
    response, msg = smtp_connection.noop()
    assert response == 250
    assert 0 # for demo purposes

使用smtp_connection的两个测试函数运行速度一样快,因为它们重用相同的实例。
如果您决定要有一个会话范围内的smtp_connection实例,您可以简单地声明它,这将表明所有的会话将重用相同的实例:

# content of conftest.py
@pytest.fixture(scope="session")
def smtp_connection():
    # 返回的夹具值将被共享给所有请求它的测试
    ...
  1. 固定装置范围(每个目录下都可以添加conftest.py文件)
    方法:在默认范围内,固定装置在测试结束时被破坏。默认每个方法都会运行这个夹具
    类:设备在拆卸类中的最后一个测试时被破坏。每个类中只运行一次这个固定装置,
    模块:在模块中最后一次测试的拆卸期间被破坏。每个py文件中只运行一次这个固定装置,比如py文件总共有10个用例,10个用例执行完了,固定装置结束运行
    包:在拆卸包中的最后一次测试时,固定装置被破坏。每个目录下只执行一次这个固定装置,目录下的所有测试用例只执行一次
    会话:该设备在测试会话结束时被销毁。运行pytest,发现1000条用例,1000条用例执行前调用一次,只调用一次,结束后停止改固定装置
  2. 动态的范围
    在某些情况下,您可能希望更改设备的范围,而不更改代码。要做到这一点,请传递一个可调用到作用域。可调用项必须返回一个具有有效作用域的字符串,
    并且在夹具定义期间只执行一次。它将用两个关键字参数来调用——fixture_name作为一个字符串和配置一个配置对象。
    这在处理需要时间进行设置的固定装置时特别有用,比如生成一个码头工人容器。您可以使用命令行参数来控制不同环境的衍生容器的范围。请参见下面的示例。
import pytest
def determine_scope(fixture_name, config):
    if config.getoption("--keep-containers", None):
        return "session"
    return "function"
def spawn_container():
    pass
@pytest.fixture(scope=determine_scope)
def docker_container():
    yield spawn_container()

2.3.4 测试后置 teardown/cleanup

当我们运行测试时,我们希望确保它们自己清理,这样它们就不会干扰任何其他测试(这样我们就不会留下大量的测试数据来膨胀系统),
teardown系统允许我们定义每个夹具自行清理所需的具体步骤。

  1. yield fixture 固定装置 (推荐)
    “Yield” 固定装置是 yield 而不是 return,使用这些装置,我们可以运行一些代码,并将一个对象传递回请求的装置/测试,就像使用其他装置一样。唯一的区别是:
    (1)yield 替代了 return
    (2)teardown代码被放到了 yield 代码后面
    (3)代码yield之前执行出现异常,则不执行yield后代码
    (4)一旦最小的计算出了一个装置的线性顺序,它将运行每一个,直到它返回或收益,然后继续到列表中的下一个装置来做同样的事情。
    (5)一旦测试完成,pytest将返回到固定装置列表中,但按照相反的顺序,取每个生成的代码,并在其中运行生成语句之后的代码。
# content of emaillib.py
class MailAdminClient:
    def create_user(self):
        return MailUser()
    def delete_user(self, user):
        # do some cleanup
        pass

class MailUser:
    def __init__(self):
        self.inbox = []
    def send_email(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
  1. 直接添加最终分配器
    虽然产量固定装置被认为是更干净、更直接的选项,但还有另一种选择,那就是将“终结器”函数直接添加到测试的请求-上下文对象中。它带来了类似的结果,但需要更冗长。
    为了使用这种方法,我们必须在夹具中请求请求上下文对象(就像我们请求另一个夹具一样),我们需要为添加拆卸代码,然后将包含拆卸代码的可调用传递给它的附加终结器方法。
    但是我们必须小心,因为pytest将在添加后运行该终结器,即使该固定装置在添加终结器后引发一个异常。因此,为了确保我们在不需要的时候不会运行终结器代码,我们只会在设备完成了我们需要拆卸的事情时添加终结器
# 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.3.5 安全的 teardown

小测试的固定系统非常强大,但它仍然由电脑运行,所以它不知道如何安全地拆卸我们扔给它的所有东西。如果我们不小心,错误位置的错误可能会影响我们的测试,这可能会很快导致进一步的问题。
安全夹具结构:
最安全和最简单的固定装置结构要求限制每个固定装置只做一个改变状态的动作,然后将它们与它们的拆卸代码捆绑在一起,如上面的电子邮件示例所示。

状态更改操作可能失败但仍然修改状态的可能性可以忽略不计,因为这些操作中的大多数往往是基于事务的(至少在状态可能落后的测试级别上是这样)。
成功的状态改变操作通过将其移动到一个单独的固定函数,并将其与其他可能失败的状态改变操作分开,那么我们的测试将最有可能离开他们发现的测试环境。

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}!"

2.3.6 安全的运行多个断言语句

请注意,这些方法只是在签名中引用自我作为一种形式。没有任何状态与实际的测试类绑定,因为它可能在 unittest.TestCase 框架。一切都由最精确的夹具系统来管理。

# 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

登录固定装置也是在类中定义的,因为并不是模块中的所有其他测试都期望成功登录,并且可能需要对另一个测试类进行一些不同的处理。例如,如果我们想编写另一个测试场景,我们可以通过在测试文件中添加类似的内容来处理它:

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)

2.3.7 固定装置可以处理请求的内容【看不懂】

固定函数可以接受请求对象,以介绍“请求”测试函数、类或模块上下文。进一步扩展之前的smtp_connection夹具示例,让我们从使用我们的夹具的测试模块中读取一个可选的服务器URL:

# content of conftest.py
import pytest
import smtplib
@pytest.fixture(scope="module")
def smtp_connection(request):
    server = getattr(request.module, "smtpserver", "smtp.gmail.com")
    smtp_connection = smtplib.SMTP(server, 587, timeout=5)
    yield smtp_connection
    print("finalizing {} ({})".format(smtp_connection, server))
    smtp_connection.close()
# content of test_anothersmtp.py
smtpserver = "mail.python.org" # will be read by smtp fixture
def test_showhelo(smtp_connection):
    assert 0, smtp_connection.helo()

smtp_connection 固定装置从模块名称空间中获取了我们的邮件服务器名称:pytest -qq --tb=short test_anothersmtp.py

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿_焦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值