通过之前一系列的解读,相信大家对于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函数结构就是一个典型的不太可靠的案例,为什么?
- 首先,
setup
这个fixture名称缺乏描述性,当然这不是最重要的问题。 - 在
setup
这一个fixture函数中做的事情太多,很多步骤不容易重用。 - 最严重的的问题来了,如果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,可以直接调用来生成测试用户。
那么,这个测试场景通常会这样去构建:
- 通过管理API创建一个用户
- 使用Selenium启动浏览器
- 进入我们网站的登录页面
- 使用创建好的用户进行登录
- 断言登录后的用户名出现在登录页的页眉中
于是乎,测试代码也就有了(注:依然是不能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}!"
可以看出,测试代码的结构就是按照上述的思路分析来进行构造的。
这种布局可能乍一看,你并不清楚user
和driver
这2个fixture函数哪个是先执行的,但是没关系,他们一定是有一个
先执行一个后执行的,之前的文章里也讲过了执行顺序,不清楚的可以往前翻看我的文章。
但是,这都不是重点。重点在于,我不管这2个fixture谁先运行,如果其中谁报错了,那么这2个fixture函数都不留下任何东西。
- 如果
driver
在user
之前运行,但是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从上到下,姑且叫做fixture1
和fixture2
吧,运行顺序是 fixture1
先。
- 我先让fixture2报错,按理说,fixture1可以正常插入删除,fixture2报错了也就是插入数据失败了。
看下运行结果:
- 接着,我恢复fixture2,再让fixture1报错。这时候,应该是fixture1直接setup就报错了,故fixture2也就不会再执行了。
看下运行结果:
所以说,现在你明白了吗?