pytest 参数化进阶

news2024/11/23 18:12:27

目录

前言:

语法

参数化误区

实践

简要回顾


前言:

pytest是一个功能强大的Python测试框架,它提供了参数化功能,可以帮助简化测试用例的编写和管理。

语法

本文就赶紧聊一聊 pytest 的参数化是怎么玩的。

@pytest.mark.parametrize

@user1ize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):
    assert eval(test_input) == expected
  • 可以自定义变量,test_input 对应的值是"3+5" "2+4" "6*9",expected 对应的值是 8 6 42,多个变量用 tuple,多个 tuple 用 list

  • 参数化的变量是引用而非复制,意味着如果值是 list 或 dict,改变值会影响后续的 test

  • 重叠产生笛卡尔积

import pytest


@user2ize("x", [0, 1])
@user3ize("y", [2, 3])
def test_foo(x, y):
    pass

@pytest.fixture()

@pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"])
def smtp_connection(request):
    smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
  • 只能使用 request.param 来引用

  • 参数化生成的 test 带有 ID,可以使用-k来筛选执行。默认是根据函数名[参数名]来的,可以使用 ids 来定义

// list
@pytest.fixture(params=[0, 1], ids=["spam", "ham"])
// function
@pytest.fixture(params=[0, 1], ids=idfn)

使用--collect-only命令行参数可以看到生成的 IDs。

参数添加 marker

我们知道了参数化后会生成多个 tests,如果有些 test 需要 marker,可以用 pytest.param 来添加

marker 方式

# content of test_expectation.py
import pytest


@user7ize(
    "test_input,expected",
    [("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)],
)
def test_eval(test_input, expected):
    assert eval(test_input) == expected

fixture 方式

# content of test_fixture_marks.py
import pytest


@pytest.fixture(params=[0, 1, pytest.param(2, marks=pytest.mark.skip)])
def data_set(request):
    return request.param
def test_data(data_set):
    pass

pytest_generate_tests

用来自定义参数化方案。使用到了 hook,hook 的知识我会写在《pytest hook》中,欢迎关注公众号 dongfanger 获取最新文章。

# content of conf.py


def pytest_generate_tests(metafunc):
    if "test_input" in metafunc.fixturenames:
        metafunc.parametrize("test_input", [0, 1])
# content of test.py


def test(test_input):
    assert test_input == 0
  • 定义在 conftest.py 文件中
  • metafunc 有 5 个属性,fixturenames,module,config,function,cls
  • metafunc.parametrize() 用来实现参数化
  • 多个 metafunc.parametrize() 的参数名不能重复,否则会报错

参数化误区

在讲示例之前,先简单分享我的菜鸡行为。假设我们现在需要对 50 个接口测试,验证某一角色的用户访问这些接口会返回 403。我的做法是,把接口请求全部参数化了,test 函数里面只有断言,伪代码大致如下

def api():
    params = []
    def func():
        return request()
    params.append(func)
    ...


@user9ize('req', api())
def test():
    res = req()
    assert res.status_code == 403

这样参数化以后,会产生50 个 tests,如果断言失败了,会单独标记为 failed,不影响其他 test 结果。咋一看还行,但是有个问题,在回归的时候,可能只需要验证其中部分接口,就没有办法灵活的调整,必须全部跑一遍才行。这是一个相对错误的示范,至于正确的应该怎么写,相信每个人心中都有一个答案,能解决问题就是 ok 的。我想表达的是,参数化要适当,不要滥用,最好只对测试数据做参数化

实践

本文的重点来了,参数化的语法比较简单,实际应用是关键。这部分通过 11 个例子,来实践一下。示例覆盖的知识点有点多,建议留大段时间细看。

1.使用 hook 添加命令行参数--all,"param1"是参数名,带--all 参数时是 range(5) == [0, 1, 2, 3, 4],生成 5 个 tests。不带参数时是 range(2)。

# content of test_compute.py


def test_compute(param1):
    assert param1 < 4

# content of conftest.py


def pytest_addoption(parser):
    parser.addoption("--all", action="store_true", help="run all combinations")
def pytest_generate_tests(metafunc):
    if "param1" in metafunc.fixturenames:
        if metafunc.config.getoption("all"):
            end = 5
        else:
            end = 2
        metafunc.parametrize("param1", range(end))

2.testdata 是测试数据,包括 2 组。test_timedistance_v0 不带 ids。test_timedistance_v1 带 list 格式的 ids。test_timedistance_v2 的 ids 为函数。test_timedistance_v3 使用 pytest.param 同时定义测试数据和 id。

# content of test_time.py
from datetime import datetime, timedelta

import pytest

testdata = [
    (datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)),
    (datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1)),
]


@user10ize("a,b,expected", testdata)
def test_timedistance_v0(a, b, expected):
    diff = a - b
    assert diff == expected


@user11ize("a,b,expected", testdata, ids=["forward", "backward"])
def test_timedistance_v1(a, b, expected):
    diff = a - b
    assert diff == expected


def idfn(val):
    if isinstance(val, (datetime,)):
        # note this wouldn't show any hours/minutes/seconds
        return val.strftime("%Y%m%d")


@user12ize("a,b,expected", testdata, ids=idfn)
def test_timedistance_v2(a, b, expected):
    diff = a - b
    assert diff == expected


@user13ize(
    "a,b,expected",
    [
        pytest.param(
            datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1), id="forward"
        ),
        pytest.param(
            datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1), id="backward"
        ),
    ],
)
def test_timedistance_v3(a, b, expected):
    diff = a - b
    assert diff == expected

3.兼容 unittest 的 testscenarios

# content of test_scenarios.py
def pytest_generate_tests(metafunc):
    idlist = []
    argvalues = []
    for scenario in metafunc.cls.scenarios:
        idlist.append(scenario[0])
        items = scenario[1].items()
        argnames = [x[0] for x in items]
        argvalues.append([x[1] for x in items])
    metafunc.parametrize(argnames, argvalues, ids=idlist, scope="class")


scenario1 = ("basic", {"attribute": "value"})
scenario2 = ("advanced", {"attribute": "value2"})


class TestSampleWithScenarios:
    scenarios = [scenario1, scenario2]

    def test_demo1(self, attribute):
        assert isinstance(attribute, str)

    def test_demo2(self, attribute):
        assert isinstance(attribute, str)

4.初始化数据库连接

# content of test_backends.py
import pytest


def test_db_initialized(db):
    # a dummy test
    if db.__class__.__name__ == "DB2":
        pytest.fail("deliberately failing for demo purposes")

# content of conftest.py
import pytest


def pytest_generate_tests(metafunc):
    if "db" in metafunc.fixturenames:
        metafunc.parametrize("db", ["d1", "d2"], indirect=True)


class DB1:
    "one database object"


class DB2:
    "alternative database object"


@pytest.fixture
def db(request):
    if request.param == "d1":
        return DB1()
    elif request.param == "d2":
        return DB2()
    else:
        raise ValueError("invalid internal test config")

5.如果不加 indirect=True,会生成 2 个 test,fixt 的值分别是"a"和"b"。如果加了 indirect=True,会先执行 fixture,fixt 的值分别是"aaa"和"bbb"。indirect=True 结合 fixture 可以在生成 test 前,对参数变量额外处理。

import pytest


@pytest.fixture
def fixt(request):
    return request.param * 3


@user16ize("fixt", ["a", "b"], indirect=True)
def test_indirect(fixt):
    assert len(fixt) == 3

6.多个参数时,indirect 赋值 list 可以指定某些变量应用 fixture,没有指定的保持原值。

# content of test_indirect_list.py
import pytest


@pytest.fixture(scope="function")
def x(request):
    return request.param * 3


@pytest.fixture(scope="function")
def y(request):
    return request.param * 2


@user19ize("x, y", [("a", "b")], indirect=["x"])
def test_indirect(x, y):
    assert x == "aaa"
    assert y == "b"

7.兼容 unittest 参数化

# content of ./test_parametrize.py
import pytest


def pytest_generate_tests(metafunc):
    # called once per each test function
    funcarglist = metafunc.cls.params[metafunc.function.__name__]
    argnames = sorted(funcarglist[0])
    metafunc.parametrize(
        argnames, [[funcargs[name] for name in argnames] for funcargs in funcarglist]
    )


class TestClass:
    # a map specifying multiple argument sets for a test method
    params = {
        "test_equals": [dict(a=1, b=2), dict(a=3, b=3)],
        "test_zerodivision": [dict(a=1, b=0)],
    }

    def test_equals(self, a, b):
        assert a == b

    def test_zerodivision(self, a, b):
        with pytest.raises(ZeroDivisionError):
            a / b

8.在不同 python 解释器之间测试对象序列化。python1 把对象 pickle-dump 到文件。python2 从文件中 pickle-load 对象。

"""
module containing a parametrized tests testing cross-python
serialization via the pickle module.
"""
import shutil
import subprocess
import textwrap

import pytest

pythonlist = ["python3.5", "python3.6", "python3.7"]


@pytest.fixture(params=pythonlist)
def python1(request, tmpdir):
    picklefile = tmpdir.join("data.pickle")
    return Python(request.param, picklefile)


@pytest.fixture(params=pythonlist)
def python2(request, python1):
    return Python(request.param, python1.picklefile)


class Python:
    def __init__(self, version, picklefile):
        self.pythonpath = shutil.which(version)
        if not self.pythonpath:
            pytest.skip("{!r} not found".format(version))
        self.picklefile = picklefile

    def dumps(self, obj):
        dumpfile = self.picklefile.dirpath("dump.py")
        dumpfile.write(
            textwrap.dedent(
                r"""
                import pickle
                f = open({!r}, 'wb')
                s = pickle.dump({!r}, f, protocol=2)
                f.close()
                """.format(
                    str(self.picklefile), obj
                )
            )
        )
        subprocess.check_call((self.pythonpath, str(dumpfile)))

    def load_and_is_true(self, expression):
        loadfile = self.picklefile.dirpath("load.py")
        loadfile.write(
            textwrap.dedent(
                r"""
                import pickle
                f = open({!r}, 'rb')
                obj = pickle.load(f)
                f.close()
                res = eval({!r})
                if not res:
                raise SystemExit(1)
                """.format(
                    str(self.picklefile), expression
                )
            )
        )
        print(loadfile)
        subprocess.check_call((self.pythonpath, str(loadfile)))


@user22ize("obj", [42, {}, {1: 3}])
def test_basic_objects(python1, python2, obj):
    python1.dumps(obj)
    python2.load_and_is_true("obj == {}".format(obj))

9.假设有个 API,basemod 是原始版本,optmod 是优化版本,验证二者结果一致。

# content of conftest.py
import pytest


@pytest.fixture(scope="session")
def basemod(request):
    return pytest.importorskip("base")


@pytest.fixture(scope="session", params=["opt1", "opt2"])
def optmod(request):
    return pytest.importorskip(request.param)

# content of base.py


def func1():
    return 1
# content of opt1.py


def func1():
    return 1.0001
# content of test_module.py
def test_func1(basemod, optmod):
    assert round(basemod.func1(), 3) == round(optmod.func1(), 3)

10.使用 pytest.param 添加 marker 和 id。

# content of test_pytest_param_example.py
import pytest


@user25ize(
    "test_input,expected",
    [
        ("3+5", 8),
        pytest.param("1+7", 8, marks=pytest.mark.basic),
        pytest.param("2+4", 6, marks=pytest.mark.basic, id="basic_2+4"),
        pytest.param(
            "6*9", 42, marks=[pytest.mark.basic, pytest.mark.xfail], id="basic_6*9"
        ),
    ],
)
def test_eval(test_input, expected):
    assert eval(test_input) == expected

11.使用 pytest.raises 让部分 test 抛出 Error。

from contextlib import contextmanager

import pytest


// 3.7+ from contextlib import nullcontext as does_not_raise
@contextmanager
def does_not_raise():
    yield


@user27ize(
    "example_input,expectation",
    [
        (3, does_not_raise()),
        (2, does_not_raise()),
        (1, does_not_raise()),
        (0, pytest.raises(ZeroDivisionError)),
    ],
)
def test_division(example_input, expectation):
    """Test how much I know division."""
    with expectation:
        assert (6 / example_input) is not None

简要回顾

本文先讲了参数化的语法,包括 marker,fixture,hook 方式,以及如何给参数添加 marker,然后重点列举了几个实战示例。参数化用好了能节省编码,达到事半功倍的效果。

  作为一位过来人也是希望大家少走一些弯路

在这里我给大家分享一些自动化测试前进之路的必须品,希望能对你带来帮助。

(WEB自动化测试、app自动化测试、接口自动化测试、持续集成、自动化测试开发、大厂面试真题、简历模板等等)

相信能使你更好的进步!

点击下方小卡片

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/761239.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

week27

这周是磨难的一周不知道NT装了多少次系统&#xff0c;删除了多少数据好消息是把BIOS和ubuntu安装地很熟练了&#xff0c;而且经过爱上了心仪的Ubuntu23.04&#xff0c;就是她了坏消息是一个学期做的笔记全都没了&#xff0c;以后不好回忆了&#xff0c;好消息是不用考试了&…

总结929

今日做了一篇阅读题&#xff0c;差点全军覆没&#xff0c;通过这篇阅读&#xff0c;主要说明了两大问题&#xff0c;一个是单词&#xff0c;背的还不够牢固&#xff0c;其二&#xff0c;语法功底还不够扎实。但说实话&#xff0c;在语法方面&#xff0c;还是下了一番功夫&#…

linux 内网批量快速传输大文件 nc

使用nc工具 传输内网宽带拉满先运行接收端 开始监听使用 ansible 拷贝脚本到其它接收端服务器批量运行接收端脚本查看nc是否运行运行发送端运行发送端脚本开始传输文件 传输内网宽带拉满 先运行接收端 开始监听 接收端脚本 re.sh #!/bin/bash #Revision: 1.0 #Author:…

动态规划(一) —— 从背包系列问题看DP

前言 动态规划可以算是算法初学者的噩梦哈哈&#xff0c;这段时间荔枝在持续学习Java后端的同时也没有忘记刷题嘿嘿嘿&#xff0c;总算把代码随想录上给出的有关动态规划的题目刷完了。接下来的几篇文章荔枝将会对于刷过的动态规划问题做出总结并给出相应的个人体会和理解。在本…

compose之沉浸式(侵入式)状态栏(隐藏状态栏)

沉浸式(侵入式)状态栏 效果图&#xff1a; 1、代码加入&#xff1a;WindowCompat.setDecorFitsSystemWindows(window, false) ComposeTestTheme {WindowCompat.setDecorFitsSystemWindows(window, false)Greeting("Android")} 2、沉浸式(侵入式)主题&#xff1a; …

消息推送(websocket)集群化解决方案

目录 需求分析解决方案实现步骤架构图配置websocket请求地址配置websocket连接前置和连接关闭监听配置websocket处理程序配置redis交换机配置redis订阅监听配置redis发布监听需求分析 及时信息传递:消息推送功能能够确保网站向用户发送及时的重要信息,包括新闻更新、促销活动…

消息队列——rabbitmq的不同工作模式

目录 Work queues 工作队列模式 Pub/Sub 订阅模式 Routing路由模式 Topics通配符模式 工作模式总结 Work queues 工作队列模式 C1和C2属于竞争关系&#xff0c;一个消息只有一个消费者可以取到。 代码部分只需要用两个消费者进程监听同一个队里即可。 两个消费者呈现竞争关…

Redis进阶底层原理-主从复制

Redis的主从节点都会记录对方的信息&#xff0c;核心还包括ReplicationID 和 offset &#xff0c; ReplicationID &#xff1a; 主从节点实例的ID &#xff0c;redis内部就是通过这个id去识别主从节点。offset&#xff1a;数据同步偏移量&#xff0c;也就是从节点每次从主节点同…

3.6 Bootstrap 导航元素

文章目录 Bootstrap 导航元素表格导航或标签胶囊式的导航菜单基本的胶囊式导航菜单垂直的胶囊式导航菜单 两端对齐的导航禁用链接下拉菜单带有下拉菜单的标签带有下拉菜单的胶囊标签页与胶囊式标签页 Bootstrap 导航元素 本文将讲解 Bootstrap 提供的用于定义导航元素的一些选项…

使用thrift编写C++服务器、客户端

在上一节《Linux 下编译 thrift》中&#xff0c;我们成功编译出了thrift的库文件&#xff0c;本节我们来编写thrift的C服务器&#xff0c;客户端。 官网 https://thrift.apache.org/tutorial/cpp.html 有thrift的C例子。在我们之前下载下来的thrift 源码根目录的tutorial/cpp目…

MySQL高级管理

目录 一、指定主键的一种方式 1.1高级操作 1.2数据表高级操作,克隆表 1.2.1 克隆表名 1.2.2备份表内容 1.3复制表 1.4删除指令 方法一&#xff1a; 方法二&#xff1a; 删除速度 二、创建临时表 三、MySQL中6种常见的约束 3.1创建主表 3.2创建从表 3.3为主表test01添加…

[Docker异常篇]解决Linux[文件异常]导致开机Docker服务无法启动

文章目录 一&#xff1a;场景复现二&#xff1a;解决思路2.1&#xff1a; 对比其他节点docker配置2.2&#xff1a;试着修改为正常节点配置2.2&#xff1a;根据上面异常显示&#xff0c;不一定是配置不对&#xff0c;可能是文件系统有损坏 三&#xff1a;解决 -> 执行命令 mo…

【机器学习算法】奇异值分解(SVD)

文章目录 奇异值分解(SVD)1.理论部分1.1特征分解(ED)1.2 奇异值分解(SVD)求解U和V求解Σ 2.应用部分2.1图像压缩2.2图像数据集成分分析2.3 数据降维(PCA的一种解法) Reference 奇异值分解(SVD) 奇异值分解(Singular Value Decomposition) 是矩阵低秩分解的一种方法&#xff0c;…

太猛了!Web安全漏洞批量扫描框架

关注【Hack分享吧】公众号&#xff0c;回复关键字【230528】获取下载链接 工具介绍 一个应用于web安全领域的漏洞批量扫描框架&#xff0c;可被应用于但不限于如下场景&#xff1a; 0Day/1Day全网概念验证(在没有测试环境(各种商业、闭源软件)或懒得搭建测试环境的情况下&…

D. Binary String Sorting

Problem - 1809D - Codeforces 思路&#xff1a;最后得到的结果就是前面是一串0后面是一串1&#xff0c;那么我们可以枚举分界点&#xff0c;如果枚举到i&#xff0c;那么就将1~i变为0&#xff0c;将i1变为1,我们发现如果一个1在1~i中&#xff0c;如果他是第i-1个&#xff0c;那…

Redis进阶底层原理-Cluster集群底层

Redis实现高可用的方案有很多中&#xff0c;先了解下高可用和分区的概念&#xff1a; 高可用是指系统在面对硬件故障、网络故障、软件错误等意外问题时&#xff0c;仍能给客户端提供正常的服务&#xff0c;尽量的减少服务的阻塞、终端现象。在高可用的方案中一般会采用冗余备份…

《洛谷深浅》第五章---数组与数据批量存储

文章目录 前言一、小鱼比可爱二、小鱼的数字游戏三、冰雹猜想四、校门外的树五、旗鼓相当的对手六、旗鼓相当的对手总结 前言 本节主要学习一维数组 和 多维数组 后边的知识我觉得 可以试着了解并不要求你掌握这么难的题目 因为ACM更多都是思维题目 所以这里把重要的题目掌握就…

【多线程系列-03】深入理解java中线程的生命周期,任务调度

多线程系列整体栏目 内容链接地址【一】深入理解进程、线程和CPU之间的关系https://blog.csdn.net/zhenghuishengq/article/details/131714191【二】java创建线程的方式到底有几种&#xff1f;(详解)https://blog.csdn.net/zhenghuishengq/article/details/127968166【三】深入…

基于树莓派实现的IO-Link 项目

IO-Link 协议 &#xff08;IEC 61131-9&#xff09; 是从传感器或执行器到 IO-Link 主站的串行半双工点对点连接。目前IO-Link 的硬应已经越来越普及。国外产品以巴鲁夫为代表。如何开发IO-link 产品&#xff1f;可以参考国外的一些开源项目。 国外有人开发了开发一个IO-Link主…

soundfile torchaudio 读取音频文件

soundfile 和 torchaudio 读取音频文件后的数据格式不同&#xff0c;前者是numpy&#xff0c;后者是tensor。前者读取后可以直接用于一些python的基础函数输入&#xff0c;后者用于pytorch的一些函数的应用。两者互换用途时候需要进行格式转换。 import soundfile as sf impor…