接口测试 pytest 接口测试 - 接口依赖 情况下实现 参数化 的问题和解决过程

耿晓 · 2024年05月13日 · 最后由 simonpatrick 回复于 2024年05月14日 · 2640 次阅读

实际场景描述

有两个接口,一个是订单创建接口,另一个是订单查询接口。在设计查询类接口的接口测试用例时,我习惯设计两条用例,一条是所有选填参数全部置空,另一条是所有选填参数全部选择相应的值。针对上述场景,我需要在编写查询接口测试用例时做参数化处理,需要传入两个参数,一个是 None,另一个则是创建订单接口创建成功后返回的新增订单 id。下文我将记录过程中遇到的问题和解决过程。

抽离成 demo

变量 a 为中间变量 - 订单 id,test_1 为新增订单接口,断言通过后将新增订单的 id 赋值给 a,test_2 为订单查询接口,参数化时传入新增订单 id,即 a 的值。

# 我期望结果是test_1断言通过后,将a的值改变成1,然后test_2测试时,a的值取1。

a = None

def test_1():
    global a
    assert 1 == 1
    a = 1

@user1ize('p1,p2',[(a,1),(a,2)])
def test_2(p1,p2):
    assert p1 == p2

运行后发现,test_2 中 a 的值取得是 None,而不是期望结果 1。查阅资料后得到结果:在 pytest 中,参数化是在收集阶段进行的,因此在收集阶段 a 的值已经被确定了,而不会在测试运行过程中动态改变。

思考:能不能在参数化数据收集阶段,收集到的 a 是一个引用,而不是具体的值,在真正用 a 作为参数传入时在取实际的值?

这个时候我想到了列表,想尝试下是否可以利用 python 列表是可变数据类型的特性解决这个问题。(参数化收集阶段收集的是列表的引用,取值的时候取的是列表中的值,有搞头,试一试)

a = [None]

def test_1():
    global a
    assert 1 == 1
    a[0] = 1

@user2ize('p1,p2',[(a,1),(a,2)])
def test_2(p1,p2):
    assert p1[0] == p2

运行后发现,确实可以!test_2 中的 a 确实是 1,而不是 None,实践证明我的想法是对的。

可我总觉着 test_1() 中的赋值语句有点别扭,如果换成 a = [1] 行不行?

a = [None]

def test_1():
    global a
    assert 1 == 1
    a = [1]

@user3ize('p1,p2',[(a,1),(a,2)])
def test_2(p1,p2):
    assert p1[0] == p2

运行后发现,又不行了。。a 又变成了 None。此处不细述心情不美丽的查找原因的过程。最终结论是在 test_1 中,当执行 a = [1] 时,它实际上创建了一个新的列表对象,并将 a 指向了这个新的列表对象,而不是修改了原来的列表对象的内容。因此由于收集阶段收集到的列表的内容没变,所以 a 始终是 None

我又觉着 a = [None] 很别扭,如果换成 a = None 行不行?

a = None

def test_1():
    global a
    assert 1 == 1
    a = 1

@user4ize('p1,p2',[([a],1),([a],2)])
def test_2(p1,p2):
    assert p1[0] == p2

运行后发现,依旧达不到预期效果,a 始终是 None。在 gpt 上我查到了这个问题的回复,回复内容如下:

在你最新的修改中,问题出在参数化时对参数的传递上。虽然你将 a 包装在列表中,但是这并不会解决问题。因为在参数化时,pytest 会将参数值进行解析并传递给测试函数,这个过程是在测试收集阶段进行的,并不会等到测试执行时才解析。所以在参数化时,a 的值已经被解析为 None,并且被传递给了参数 p1,后续在测试执行时修改了 a 的值也不会影响参数化阶段已经确定的 p1 的值。

但我其实并不是很理解,尤其是答复中的 "因为在参数化时,pytest 会将参数值进行解析并传递给测试函数,这个过程是在测试收集阶段进行的,并不会等到测试执行时才解析。",为什么 [a] 会被解析成 None,而不是 [None]",如果有佬哥佬姐有更通俗易懂的原因,希望留言,解惑。

上述就是我对接口依赖情况下实现参数化的思考和时间,虽然在实例 1 中我们达到了预期效果,但我想可能在代码可读性上还有一些疑惑,有可能团队中的其他伙伴会对 a = [None] 这行代码中为什么要把 None 放在列表里不明所以。最后,我又想到了另一种能达到预期且代码可读性也比较好的解决办法。

a = None

def test_1():
    global a
    assert 1 == 1
    a = 1

@user5ize('p1,p2',[(None,1),(None,2)])
def test_2(p1,p2):
    if p1 is None:
        p1 = a
    assert p1 == p2

上面的代码即能实现预期效果,代码可读性也增强了不少,所以我就不再解释了。

作为测试小学生,在面对"接口依赖情况下实现参数化"这个问题时,上述是我能想到的解决办法,如果有佬哥佬姐有更好的解决办法,希望留言,解惑🍻

最佳回复

文中提到的参数化用的是@pytest.mark.parametrize,可能由于编辑器问题,发布后变成了@user5ize☀

共收到 4 条回复 时间 点赞

文中提到的参数化用的是@pytest.mark.parametrize,可能由于编辑器问题,发布后变成了@user5ize☀

对这个问题的间接回答:
为什么一定都要参数化呢?专门写一个有 order id 的,其他错误的用参数化一下不行吗?
为了这个事情,其实花的时间更多了吧,比直接写个测试用例专门测有 order id 的场景。

对这个问题的直接回答:

  1. @pytest.mark.parametrize 需要的是一个 set 的列表作为参数化列表
  2. 可以使用如下的方式可能会更可读一些:

cases = CaseLoader.load_cases() ## load_cases可以让你的order api在这个里面调用

@user2ize("p1,P2", cases)
def test_abc():
    test_it()
  1. 也可以使用一些 pytest 执行顺序插件,强制让订单接口执行
simonpatrick 回复

load_cases() 里面的 order_id 是创建订单接口创建成功后返回的,并非测试前写死的。创建订单接口本身也要作为待测试接口,在此情况下,我是不是可以理解为在 load_cases() 里面将 create 接口默认认为是测试通过的接口,然后根据创建订单接口的返回值来构造 return 的值

耿晓 回复

是的,可以这样的,顺手写了一个 pytest 的一些使用,https://testerhome.com/articles/39759, 仅供参考。

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