欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

荐 pytest框架走出 test -> fixture <-> fixture 调用限制的魔咒

程序员文章站 2022-03-23 09:21:00
文章目录1. 前言2. 外部入参走出fixture魔咒3. fixture函数入参方法4. 把case失败的接口会话写入报告1. 前言最近把之前写的基于unitest的测试项目迁到pytest了,虽然pytest无缝支持unitest的写法,但是还是按照pytest的规范改动了不少。本文就来记录一下实际使用过程中遇到的问题。pytest有一个fixture概念,甚至推荐setup、 setdown也用fixture的yield来实现。*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