荐 pytest框架走出 test -> fixture <-> fixture 调用限制的魔咒
1. 前言
最近把之前写的基于unitest的测试项目迁到pytest了,虽然pytest无缝支持unitest的写法,但是还是按照pytest的规范改动了不少。本文就来记录一下实际使用过程中遇到的问题。
pytest有一个fixture概念,甚至推荐setup、 setdown也用fixture的yield来实现。
*fixture不能手动调用,只能使用在其他fixture函数或test函数以入参形式使用。
2. 外部入参走出fixture魔咒
一般测试框架都有一个需求就是通过命令行来指定当前测试环境
这里可以使用pytest内置pytest_addoption函数来收集入参
conftest.py
# pytest_addoption是保留函数
# --env 是接收命令的指令,例: pytest --env dev
# dest 是它的名字,读取参数时要用到
def pytest_addoption(parser):
parser.addoption("--env", action="store", dest="environment", default="dev", help="environment: dev, dev-01, dev-02")
# 这是自定义的fixture函数
# request.config.getoption('environment') 来读取上面函数中收集到的入参的dest名字
# 这个函数的意义是通过--dev 入参去读取指定的 dev.yaml dev-01.yaml 配置文件
@pytest.fixture(scope="session", autouse=True)
def env(request):
config_path = os.path.join(request.config.rootdir, "config", f"{request.config.getoption('environment')}.yaml")
with open(config_path) as f:
env_config = yaml.load(f.read(), Loader=yaml.SafeLoader)
return env_config
使用示例
get_assessment_list_test.py
# 这里是一个测试类中的fixture函数
# env['ASSESSMENT_URL'] 读取了--env 入参对应的yaml文件中的ASSESSMENT_URL值
class GetAssessmentListTest:
@pytest.fixture()
def api(self, env, login_enterprise):
api = GraphqlApi()
api.url = env["ASSESSMENT_URL"] + api.path
... 省略
敲重点!!!
测试环境以 --dev 入参方式被接收,以 request.config.getoption(‘dest_name’)来使用
但是很快我发现request是一个fixture,所以只能在fixture 和test函数中使用!??
那我的工具类,封装的接口类怎么搞?难道要从test函数一层一层往里丢?
所以我想到了一个很鸡贼的方法,那就是扔进环境变量
conftest.py
# 接收命令行入参方式不变
def pytest_addoption(parser):
parser.addoption("--env", action="store", dest="environment", default="dev", help="environment: dev, dev-01, dev-02")
# 把接收到的外部入参写入环境变量,这样就不受框架限制了
# scope="session" 是全局的意思,autouse是自动执行的意思,执行顺序在最前面
@pytest.fixture(scope="session", autouse=True)
def add_system_env(request):
# 写入环境变量
os.environ["TEST_ENV"] = request.config.getoption('environment')
使用示例
由于autouse是pytest框架一启动就执行了,所以执行道test类test函数的时候,环境变量里已经有值了。所以想怎么用,就怎么用。
# os.environ是一个字典
print(os.environ.get("TEST_ENV"))
# 输出的结果 {"TEST_ENV": "dev"}
应用场景
我觉得读取数据库就很合适,因为我不想把查找数据库维护在test类中,或者就算维护在fixture里也要test类传一遍,这很繁琐也不符合逻辑。
class Mysql:
def __init__(self, mysql_conf=None):
if not mysql_conf:
mysql_conf = Tool.get_config("MYSQL_CONFIG")
self.conn = pymysql.connect(**mysql_conf, autocommit=False)
self.cur = self.conn.cursor(pymysql.cursors.DictCursor)
... 省略代码
*我把写入/ 读取os.environ封装成Tool类方法了,这里就不贴代码了。这里的Tool.get_config就是从环境变量取到dev环境,再去读dev.yaml文件。
3. fixture函数入参方法
先给一个错误的示范
根据我们正常理解,可能会这样写,IDE也没报错,但是执行肯定会报错。
import pytest
class TwoSumTest():
@pytest.fixture()
def two_sum(self, a, b):
yield a + b
def test_two_sum(self, two_sum):
print("two_sum", two_sum(1, 2))
这里有个很鸡贼的方法就是返回一个其他函数
*fixture甚至可以返回一个类对象
import pytest
class TwoSumTest():
@pytest.fixture()
def two_sum(self):
def _two_sum(a, b):
return a + b
yield _two_sum
def test_two_sum(self, two_sum):
print("two_sum", two_sum(1, 2))
应用场景
我觉得登录就很适合,登录的账密入参总不能hard code吧。
conftest.py
@pytest.fixture(scope="session")
def login_enterprise(env):
def _login_enterprise(username, password):
from request.enterprise.login.enterprise_user_login_request import EnterpriseUserLoginRequest
login_api = EnterpriseUserLoginRequest()
login_api.url = env["ENTERPRISE_URL"] + login_api.path
login_api.data_variables = {"login": username, "password": password}
cookies = login_api.request().cookies
return cookies
yield _login_enterprise
4. 把case失败的接口会话写入报告
这一段是我从其他大佬那 copy 的失败后截图的的一段
conftest.py
@pytest.mark.hookwrapper
def pytest_runtest_makereport(item):
outcome = yield
report = outcome.get_result()
extra = getattr(report, 'extra', [])
# 如果错误了
if report.when == 'call' or report.when == "setup":
xfail = hasattr(report, 'wasxfail')
if (report.skipped and xfail) or (report.failed and not xfail):
try:
html = item.funcargs["api"].info
extra.append(pytest_html.extras.html(f'<pre>{html}</pre>'))
except Exception:
pass
report.extra = extra
report.description = str(item.function.__doc__)
report.nodeid = report.nodeid.encode("utf-8").decode("unicode_escape") # 解决乱码
这个需要很多前期工作,只能意会的讲一下了,下面这2行代码是核心内容
· 从item.funcargs字段中取出一个对象
· 把取出的东西放进html的extras里
html = item.funcargs["api"].info
extra.append(pytest_html.extras.html(f'<pre>{html}</pre>'))
那么这个item.funcargs[“api”]是怎么来的呢?其实是我们test函数中的fixture入参
get_assessment_list_test.py
class GetAssessmentListTest:
@pytest.fixture()
def api(self, env, login_enterprise):
api = GraphqlApi()
api.url = env["ASSESSMENT_URL"] + api.path
... 省略
# 这个api是我定义的接口类,包含了被测接口的属性 & 方法
def test_get_assessment_list(self, api):
"""正常流程
:return:
"""
_api = api
# 这个请求函数,除了自身逻辑外,还会把接口的method, url, request, response 拼接后扔进self.info
_api.request()
_api.assertion("0x000000")
graphql_api.py
def request(self):
... 省略代码
self.info = Log.generate_log_info(self)
... 省略代码
本文地址:https://blog.csdn.net/tomoya_chen/article/details/107890481
上一篇: 第一章总结(计网)