Pytest 巧用环境变量实现常规方法使用 fixture 对象、fixture 函数入参方法、免登录策略示例

1. 前言

最近把之前写的基于 unitest 的测试项目迁到pytest 了,虽然 pytest 无缝支持 unitest 的写法,但是还是按照 pytest 的规范改动了不少。本文就来记录一下实际使用过程中遇到的问题。

pytest有一个fixture概念,甚至推荐setup、 setdown也用fixture的yield来实现。
*fixture不能手动调用,只能使用在其他fixture函数或test函数以入参形式使用。

2. 在常规函数中使用外部传参

一般测试框架都有一个需求就是通过命令行来指定当前测试环境
这里可以使用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
        ... 省略

敲重点!!!
测试环境以 --env 入参方式被接收,以 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. 相同用户免登录策略

有些公司的登录接口,会做一个QPS限制,请求太多会响应"请勿频繁操作"。所以我们一个test类登录一次,可能都会太频繁。
正在想怎么解决的时候,突然想起来,python万物皆对象,func本身也是对象。再利用fixture的特性岂不是。。

conftest.py

@pytest.fixture(scope="class")
def login_with_phone():
    def __login_with_phone(phone, code):
        # 如果已经有cookies了
        if hasattr(login_with_phone, "cookies") and login_with_phone.cookies:
            # 并且用户名密码与之前成功登录的相同才登录
            if login_with_phone.username == phone and login_with_phone.password == code:
                return login_with_phone.cookies

            # 登录操作
        from request.enterprise.login.enterprise_user_login_request import EnterpriseUserLoginRequest
        login_api = EnterpriseUserLoginRequest()
        cookies = login_api.login_and_get_cookies(phone, code)

        # 如果登录成功了,把cookies传递给外部函数的cookies属性
        if login_api.request().isSuccess:
            login_with_phone.cookies = cookies
        login_with_phone.username = phone
        login_with_phone.password = code

        return cookies

    yield __login_with_phone

这样的话,会如果检查如果本次登录与上次登录账密一样,就继承cookies,否则重新登录。
*注意这里的fixture的scope需要改成class或module。session和function都不行。当然,如果你只希望同一文件内共享,那么就用function好了。

你甚至还可以使用骚操作,即使交错着登录不同的账号也总能共享账密。


@pytest.fixture(scope="class")
def login_with_phone():
    def __login_with_phone(phone, code):
        prev_user_key = phone + code
        if hasattr(login_with_phone, "cookies"):
            # 如果用户存在于用户池,并且密码也一样,返回之前这个用户的cookies
            if prev_user_key in login_with_phone.cookies and login_with_phone.cookies[prev_user_key]:
                return login_with_phone.cookies[prev_user_key]
        else:
            login_with_phone.cookies = {}

        # 登录操作
        from request.enterprise.login.enterprise_user_login_request import EnterpriseUserLoginRequest
        login_api = EnterpriseUserLoginRequest()
        cookies = login_api.login_and_get_cookies(phone, code)

        # 如果登录成功了,把用户信息与cookies存入用户池中
        if login_api.is_success:
            login_with_phone.cookies[prev_user_key] = cookies
        return cookies

    yield __login_with_phone


@pytest.fixture(scope="class")
def login_with_user(login_main_site_with_phone):
    def __login_with_user(user: dict) -> str:
        if isinstance(user, dict) and 'phone' in user and 'code' in user:
            return login_with_phone(user['phone'], user['code'])

    yield __login_with_user
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值