【pytest官方文档】解读fixtures - 9. 什么样的fixture结构,用起来最可靠?

通过之前一系列的解读,相信大家对于fixture已经有了更多的理解。fixture功能强大,
我觉得用来处理setup、teardown非常的灵活,好用。

但是,毕竟它也只是一段程序代码,虽然可以帮我们做setup、teardown的处理,但是并不代表任何情况下都可以完美处理掉。
拿teardown来说,假如我们写的代码不小心报错了,导致该删掉的没删掉,那么就可能会导致后续一些奇怪的问题发生。

一、不可靠的fixture函数长啥样?

一起先来看下官方给的代码示例(文末会用我自己的实践代码来表述核心思想):

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_emai(email, receiving_user)
    yield receiving_user, email
    receiving_user.delete_email(email)
    admin_client.delete_user(sending_user)
    admin_client.delete_user(receiving_user)


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

这段代码你别急着copy过去运行,因为我试过不行,我们主要用它来辅助描述一些概念。
先来看下这段代码是干嘛使的:

  • 这是一个测试用例代码,测试用户之间收发邮件的。
  • test_email_received是测试函数,并且用了一个叫setup的fixture函数。
  • setup就是这个fixture函数,yield之前的代码主要是用来创建2个用户,一个是发送者,另一个是接收者。
    并且发送了邮件
  • yield之后的代码就是在测试结束后,接收者删除邮件,客户端删除这2个测试用户。

思路清晰,似乎没啥问题。但是,这里的fixture函数结构就是一个典型的不太可靠的案例,为什么?

  1. 首先,setup这个fixture名称缺乏描述性,当然这不是最重要的问题。
  2. setup这一个fixture函数中做的事情太多,很多步骤不容易重用。
  3. 最严重的的问题来了,如果yield之前的任何代码报错,那么yield之后的teardown代码都不会运行。

这就是会出现本章开头提到的问题,有些数据被生成出来,最后没有被删除掉。
虽然,在之前我们也学到了用addfinalizer来处理,可以让teardown代码继续执行,但是不得不说,那种写法还是
比较复杂的,而且阅读性跟维护性都不是很好。

二、如何让fixture函数更可靠

其实很多事情要想可靠,首先必须要简单。
上面的fixture不是一个里面做的事情太多了吗?那么就把他们都拆出来,用的时候再把他们重新绑定在一起就好了。

1、官方示例代码1

对于上面发送邮件的测试代码,就可以改成下面这种:

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
    admin_client.delete_user(user)


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


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

可以看出,每个fixture函数里只做一种状态的操作。
比如sending_user里,就只做发送者的创建跟删除,receiving_user里就只做接收者的创建跟删除。mail_admin是用来生成一个类似管理员的
客户端,用来创建用户,也把它独立成一个fixture函数,用的时候就可以跟另外2个绑定在一起使用了。

看到这可能还有点不明白,没关系,继续看下一个官方示例。

2、官方示例代码2

这是一个web自动化的测试用例。
假如,我们有一个登录页面,需要进行登录测试。为了方便测试,我们还有一个管理员的api,可以直接调用来生成测试用户。
那么,这个测试场景通常会这样去构建:

  1. 通过管理API创建一个用户
  2. 使用Selenium启动浏览器
  3. 进入我们网站的登录页面
  4. 使用创建好的用户进行登录
  5. 断言登录后的用户名出现在登录页的页眉中

于是乎,测试代码也就有了(注:依然是不能copy运行的代码,假设代码中所有需依赖的代码都存在):

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

可以看出,测试代码的结构就是按照上述的思路分析来进行构造的。
这种布局可能乍一看,你并不清楚userdriver这2个fixture函数哪个是先执行的,但是没关系,他们一定是有一个
先执行一个后执行的,之前的文章里也讲过了执行顺序,不清楚的可以往前翻看我的文章。

但是,这都不是重点。重点在于,我不管这2个fixture谁先运行,如果其中谁报错了,那么这2个fixture函数都不留下任何东西。

  • 如果driveruser之前运行,但是user报错了。
    那么,driver里的teardown依然会执行,浏览器驱动会退出。并且,因为user报错了,所以测试用户并没有被创建出来。
  • 如果,driver运行的时候就报错了,那么浏览器驱动都不会进行初始化。而运行顺序排后面到user根本都不会去运行了,
    所以也就更不会生成测试用户了。

3、我自己的实战代码

是不是有点眉目了?但是还是不能彻底理解?没关系,我在项目的实战代码中就是这么用的,可以直接拿来验证效果。
这里我贴出来2个fixture函数,有着调用的关系,测试case的代码就不用贴了。

做的事情也很简单,就是往两个关联表的插数据,第一个是主表,另一个是附属表。
在yield之前是setup操作,负责插入数据,在yield之后是teardown操作,负责删除数据。
在format部分,我会做手脚分别让这2个fixture函数报错,来看下互相的影响。

@pytest.fixture()
def insert_sm_purchase_order():
    """
    插入采购订单表的数据
    :return:
    """
    db = DB("db_info")
    purchase_order_sn = "CGN016" + deal_date() + str(random_int(4))
    purchase_order_id = db.get_table_usable_latest_id("tcwms", "sm_purchase_order")
    insert_sql = """
        INSERT INTO `tcw`
        ...
        sql语句直接省略了
    """.format(purchase_order_id, purchase_order_sn, C_TIME, int(C_TIME))# 这里让其报错
    db.exec_sql(insert_sql)
    db.close()

    yield purchase_order_id, purchase_order_sn

    db = DB("db_info")
    db.exec_sql("DELETE FROM `tcwm... sql语句直接省略了)
    db.close()


@pytest.fixture()
def insert_sm_purchase_order_goods(insert_sm_purchase_order):
    """
    插入采购商品表的数据
    :return:
    """
    db = DB("db_info")
    purchase_order_goods_id = db.get_table_usable_latest_id("tcwms", "sm_purchase_order_goods")
    po_id = insert_sm_purchase_order[0]
    purchase_order_sn = insert_sm_purchase_order[1]
    insert_sql = """
        INSERT INTO `tcw`
        ...
        sql语句直接省略了
    """.format(purchase_order_goods_id, po_id, C_TIME)# 这里让其报错
    db.exec_sql(insert_sql)
    db.close()
    yield purchase_order_goods_id, po_id, purchase_order_sn
    db = DB("tcwms_db_info")
    db.exec_sql("DELETE FROM `tcwm... sql语句直接省略了)
    db.close()

这里2个fixture从上到下,姑且叫做fixture1fixture2吧,运行顺序是 fixture1先。

  • 我先让fixture2报错,按理说,fixture1可以正常插入删除,fixture2报错了也就是插入数据失败了。
    看下运行结果:


  • 接着,我恢复fixture2,再让fixture1报错。这时候,应该是fixture1直接setup就报错了,故fixture2也就不会再执行了。
    看下运行结果:

所以说,现在你明白了吗?

posted @ 2021-03-27 10:48  把苹果咬哭的测试笔记  阅读(290)  评论(0编辑  收藏  举报