接口测试 这次,我掌握了 pytest 中 fixture 的使用及 pytest 运行测试的加载顺序

耿晓 · 2024年05月15日 · 最后由 耿晓 回复于 2024年05月21日 · 3904 次阅读

先说经验总结

  • pytest 会在测试函数调用时自动查找具有相同名称的 fixture,并将其注入到测试函数中。
  • pytest 在执行测试时会先进行测试用例的收集,然后再执行测试。

实操:[造数函数] 和 [测试用例] 都需要调用登录接口获取用户 token,既然都需要调用登录接口,那会不会出现 token 失效的情况呢?

例如有一个订单查询接口 orderList,调用 orderList 需要传入的参数为:token、orderType、submitState、page 和 limit,其中 submitState 的可选值是固定的 [None,1,2],orderType 的可选值是由接口 getOrderType 返回的,调用 getOrderType 需要传入 token。在这种场景下设计用例就引发了一系列的思考。

首先我共有三个.py 文件,test.py,conftest.py 和 case_data.py,内容大致如下:

在写 case_data 时,先后共写过两个版本,我描述一下思考过程,当前为第一个版本。

# conftest.py
@pytest.fixture(scope="session")
def tf_stu_token():
    base_url = Environment.HOST_LAPI + 'login'
    data = {
            'userphone': Environment.userphone_stu,
            'password': CT.to_md5(Environment.password_stu),
            'phonecode': 86,
            'type': 1
            }
    resp = requests.post(url=base_url, data=data)
    assert resp.status_code == 200
    token = resp.json()["data"]["token"]
    return token
# case_data.py V1

def getOrderType(tf_stu_token):  # !问题就出在这里,在非测试函数的地方引用fixture并不会生效,但当时不知道
    url = Environment.HOST_LAPI + 'getOrderType'
    data = {
        'token':tf_stu_token,
    }
    resp = requests.post(url=url,data=data)
    return resp.json()['data']  # 会返回[0,1,2,3,4]

def data_orderList():
    total = []

    orderType_list = getOrderType()
    submitState_list = [None,1,2]

    for i in range(5):
        item = []
        case = {}
        case['OrderType'] = orderType_list[i % len(orderType_list)]
        case['submitState'] = submitState_list[i % len(submitState_list)]

        item.append(case)

        expect = {}
        expect['code'] = 2000
        item.append(expect)

        item.append('正常入参')

        total.append(item)

    # token为空
    total.append([{'OrderType': 0,'submitState': None}, {'code': 4001}, 'token为空'])

    return total

'''
这个total最终为包含6条测试数据的列表:
 [
    [{'OrderType': 0,'submitState': None}, {'code': 2000}, '正常入参'],
    [{'OrderType': 1,'submitState': 1}, {'code': 2000}, '正常入参'],
    [{'OrderType': 2,'submitState': 2}, {'code': 2000}, '正常入参'],
    [{'OrderType': 3,'submitState': None}, {'code': 2000}, '正常入参'],
    [{'OrderType': 4,'submitState': 1}, {'code': 2000}, '正常入参'],
    [{'OrderType': 0,'submitState': None}, {'code': 4001}, 'token为空']
]
'''
# test.py
import pytest
import requests
import case_data as CD

@ pytest.mark.parametrize("case, expect, desc", CD.data_orderList())
def test_orderList(tf_stu_token, case, expect, desc):
    url = Environment.HOST_LAPI + 'orderList'
    data = {
        'token': tf_stu_token,
        'orderType':case['orderType'],
        'submitState':case['submitState'],
        'page': 1,
        'limit': 20
    }
    resp = requests.post(url=url, data=data)
    assert resp.json()['code'] == expect['code']

测试后发现,造数函数报错,原因是调用 getOrderType 时缺少参数 tf_stu_token。当时就很纳闷,我隐约记得 fixture 函数不需要显式调用呀,不应该是运行的时候会自动加载么。报错信息大致如下:

查阅后发现,pytest 会在测试函数中调用时自动查找具有相同名称的 fixture,并将其注入到测试函数中。非测试函数并不能使用 fixture。 也有资料说 fixture 可以被非测试函数调用,但几经实践后并没有成功。
于是为了在 case_data.py 中获取 token,我决定在 case_data.py 中再另行调用一遍登录接口。修改后的 case_data.py 如下:

# case_data.py V2

def get_stu_token(userphone, password):
    base_url = Environment.HOST_LAPI + 'login'
    data = {'userphone': userphone,
            'password': CT.to_md5(password),
            'phonecode': 86,
            'type': 1
            }
    response = requests.post(url=base_url, data=data)
    token = response.json()["data"]["token"]
    return token

token_stu = get_stu_token(Environment.userphone_stu, Environment.password_stu)

def getOrderType():  # !问题就出在这里,在非测试函数的地方引用fixture并不会生效,但当时不知道
    url = Environment.HOST_LAPI + 'getOrderType'
    data = {
        'token':token_stu,
    }
    resp = requests.post(url=url,data=data)
    return resp.json()['data']  # 会返回[0,1,2,3,4]

def data_orderList():
    total = []

    orderType_list = getOrderType()
    submitState_list = [None,1,2]

    for i in range(5):
        item = []
        case = {}
        case['OrderType'] = orderType_list[i % len(orderType_list)]
        case['submitState'] = submitState_list[i % len(submitState_list)]

        item.append(case)

        expect = {}
        expect['code'] = 2000
        item.append(expect)

        item.append('正常入参')

        total.append(item)

    # token为空
    total.append([{'OrderType': 0,'submitState': None}, {'code': 4001}, 'token为空'])

    return total

运行后可以正常运行,但我当时心中又有一个疑问,我在造数脚本里调用登录接口,会不会导致测试脚本里的 token 失效?经过实践后得到结论:在造数函数中调用登录接口并不会致使测试脚本中的 token 失效,原因是pytest 在执行测试时会先进行测试用例的收集,然后再执行测试,放在当前情景中,如果有影响,也是测试用例中调用登录接口后会致使造数脚本中的 token 失效,可造数接口已经提供完价值了,token 失不失效也没有任何影响了。

文中提到的参数化用的是@pytest.mark.parametrize,可能由于编辑器问题,发布后变成了@user5ize,所以我在 @ 后增加了一个空格

共收到 8 条回复 时间 点赞

为什么不把 token 或 cookie 这些数据保存到 yaml 文件里面呢,这样更加方便

还可以将夹具传入 test 测试函数后,把造数的 CD.data_orderList() 放在 test 函数内部,这样 data_orderList() 就可以用 test 函数的夹具了,还省了参数化,缺点是没有分离数据和测试逻辑

Messier64 回复

如果把造数放在用例内部,那最后的测试报告应该就会显示一条 case 吧,像上面的例子,如果把造数函数放在参数化里,allure 报告就会显示 6 条 case,以至于哪条 case 不通过显示的也比较明显🍻

4楼 已删除
耿晓 回复

有道理,暂时想不出更好的了,用 getOrderType 夹具调用 token 夹具的思路也只会有 1 条 case,网上搜有在 parametrize()里放 fixture 的但我没试出来。还能想到的思路就是可以把 CD.data_orderList() 用 yield 做成生成器,每次返回一条,再用 parametrize 或 repeat 控制下用例测试次数,但这样搞就感觉更麻烦了

Messier64 回复

在 parametrize()里放 fixture 需要用 pytest-lazy-fixture。我有用过,像下面这样:

耿晓 回复

学到了,谢谢👍 👍 👍

耿晓 回复

这里面只有一条 case 呀,一般不都是一个 test 的测试方法作为一个用例么。造数方法也统计进用例数量里有啥意义呢。

藕片 回复

针对一个测试点我们会设计多条用例,总不能一个测试点就一条 case 吧,像这样:

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册