• Pytest插件开发


    Pytest测试框架功能非常多,它其实就是由一组插件组成的,具有大量的插件,可以通过插件来扩展、定制功能,能满足大部分的测试需求。本文介绍pytest插件的开发方法,帮助更好的理解pytest测试框架。

    pytest插件介绍

    pytest的三种插件

    pytest插件通过hook函数来实现,pytest主要包括以下三种插件

    • 内置插件:pytest内部的_pytest目录中加载:Libsite-packages\_pytesthookspec.py
    • 外部插件:pip install 插件,通过setuptools的Entry points机制来发现外部插件,可用插件列表:https://docs.pytest.org/en/latest/reference/plugin_list.html
    • 本地插件:conftest.py插件,pytest自动模块发现机制,在项目根目录下的conftest文件起到全局作用,在项目下的子目录中的conftest.py文件作用范围只能在该层级及以下目录生效。

    他们的加载顺序为:

    1. 内置插件
    2. 外部插件
    3. 本地插件

    pytest的hook函数

    hook函数(钩子函数)是程序中预留的函数(相当于暴露了一个钩子),如果我们需要在程序某个步骤执行某个操作,我们就直接重写特定的钩子函数(挂载到钩子上),这样就实现了我们要增加的功能。没有挂载或者注册钩子时,它就是空的,也就是没有执行任何操作。

    Pytest 的hook函数可查看Libsite-packages\_pytesthookspec.py 文件, Pytest hook函数的执行顺序如下(https://github.com/pytest-dev/pytest/issues/3261):

    root
    └── pytest_cmdline_main
        ├── pytest_plugin_registered
        ├── pytest_configure
        │   └── pytest_plugin_registered
        ├── pytest_sessionstart
        │   ├── pytest_plugin_registered
        │   └── pytest_report_header
        ├── pytest_collection
        │   ├── pytest_collectstart
        │   ├── pytest_make_collect_report
        │   │   ├── pytest_collect_file
        │   │   │   └── pytest_pycollect_makemodule
        │   │   └── pytest_pycollect_makeitem
        │   │       └── pytest_generate_tests
        │   │           └── pytest_make_parametrize_id
        │   ├── pytest_collectreport
        │   ├── pytest_itemcollected
        │   ├── pytest_collection_modifyitems
        │   └── pytest_collection_finish
        │       └── pytest_report_collectionfinish
        ├── pytest_runtestloop
        │   └── pytest_runtest_protocol
        │       ├── pytest_runtest_logstart
        │       ├── pytest_runtest_setup
        │       │   └── pytest_fixture_setup
        │       ├── pytest_runtest_makereport
        │       ├── pytest_runtest_logreport
        │       │   └── pytest_report_teststatus
        │       ├── pytest_runtest_call
        │       │   └── pytest_pyfunc_call
        │       ├── pytest_runtest_teardown
        │       │   └── pytest_fixture_post_finalizer
        │       └── pytest_runtest_logfinish
        ├── pytest_sessionfinish
        │   └── pytest_terminal_summary
        └── pytest_unconfigure
    

    我们可以对上面的hook函数进行改写,实现某些功能。我在以前的pytest文章中(Pytest测试框架(三):pytest fixture 用法)介绍了fixture 插件的用法,fixture实现的功能其实也对pytest的hook函数进行了改写,比如pytest_generate_tests,pytest_sessionstart等hook函数,大家如果感兴趣可以查看源码:Libsite-packages\_pytestfixtures.py

    pluggy插件系统

    前面简要介绍了pytest插件,这些插件是怎么管理的呢?pytest的大量插件使用pluggy进行管理,pluggy是pytest使用的一个插件系统,用于pytest插件的管理和钩子调用。也就是说,pluggy使pytest具有了钩子功能,实现主机(主程序)与插件的连接。

    pytest插件:中文编码

    先写一个测试用例test_hook.py:

    #!/usr/bin/python3
    #-*-coding:utf-8-*-
    import pytest
    
    @pytest.mark.parametrize("name",["张三","李四"])
    def test_name(name):
        print(name)
    

    执行:

    $ pytest -vs test_hook.py::test_name
    

    结果(只截取部分):

    test_hook.py::test_name[u5f20u4e09] 张三
    PASSED
    test_hook.py::test_name[u674eu56db] 李四
    PASSED
    
    

    我们发现用例名字编码格式为Unicode,无法显示中文。

    怎么解决呢?我们可以对pytest hook函数pytest_collection_modifyitems()进行重写:

    def pytest_collection_modifyitems(
        session: "Session", config: "Config", items: List["Item"]
    ) -> None:
        """ called after collection has been performed, may filter or re-order
        the items in-place.
    
        :param _pytest.main.Session session: the pytest session object
        :param _pytest.config.Config config: pytest config object
        :param List[_pytest.nodes.Item] items: list of item objects
        """
    

    根据注释我们知道这个hook函数在用例收集完成后进行调用,可对用例进行过滤或者重新排序(修改用例执行顺序的pytest-ordering插件就修改了这个hook函数)

    接下来开始修改这个hook函数,对用例名进行解码并反转用例顺序,在测试用例同级目录下新建conftest.py函数,修改代码如下:

    #!/usr/bin/python3
    #-*-coding:utf-8-*-
    
    from typing import List
    
    def pytest_collection_modifyitems(session, config, items: List):
        for item in items:
            item.name = item.name.encode('utf-8').decode('unicode-escape')
            item._nodeid = item.nodeid.encode('utf-8').decode('unicode-escape')
        items.reverse()
    

    items就是收集到的测试用例,可对它进行各种操作。

    再次执行:

    $ pytest -vs test_hook.py::test_name
    

    结果(只截取部分):

    test_hook.py::test_name[李四] 李四
    PASSED
    test_hook.py::test_name[张三] 张三
    PASSED
    

    解码成功,并且顺序反转了。

    添加命令行参数

    通过改写hook函数pytest_addoption()可以实现添加自定义的命令行参数,几乎每个pytest 插件都会使用这个hook方法。下面在conftest.py中改写pytest_addoption()方法:

    # 添加一个命令行参数
    def pytest_addoption(parser, pluginmanager):
        mygroup = parser.getgroup("testgroup") #group将下面所有的 optiongroup都展示在这个group下。
        mygroup.addoption("--env", #注册一个命令行选项
            default='test',  # 参数的默认值
            dest = 'env',  # 存储的变量
            help = 'set your run env' #帮助提示参数的描述信息
        )
    

    然后我们在命令行中输入pytest -h,在打印的帮助信息中,我们可以看到添加的自定义参数:

    testgroup:
      --env=ENV             set your run env
    

    接下来获取这个参数,在conftest.py中添加如下代码:

    @pytest.fixture(scope='session')
    def cmdoption(request):
        env = request.config.getoption("--env", default='test')
        if env == "test":
            print("test环境")
            datapath = "data/test/data.yml"
        if env == "develop":
            print("开发环境")
            datapath = "data/develop/data.yml"
        with open(datapath) as f:
            datas = yaml.safe_load(f)
    
        return env,datas
    

    测试数据:

    编写测试用例:

    def test_env(cmdoption):
        env,datas = cmdoption
        host = datas['env']['host']
        port = datas['env']['port']
        url = str(host) + ":" + str(port)
        print(url)
    

    执行测试用例:

    $ pytest -vs test_hook.py::test_env
    

    结果(只截取部分):

    test_hook.py::test_env test环境
    http://192.168.11.1:4567
    PASSED
    

    传递参数:

    $ pytest -vs --env develop test_hook.py::test_env
    

    结果(只截取部分):

    test_hook.py::test_env 开发环境
    http://192.168.0.1:1234
    PASSED
    

    传递成功

    打包发布

    你的Python插件开发完成后,可以对它进行打包发布,方便给别人使用,打包后也可以发布代码到到PyPI上,可参考Python打包文档:https://packaging.python.org/tutorials/packaging-projects/ ,下面介绍Python打包过程。

    1、创建包文件

    pytest-encode/
    ├── LICENSE
    ├── README.md
    ├── setup.py
    ├── pytest_encode/
    │   └── __init__.py
    └──tests/
        └── test_encode.py
    

    setup.py是一个构建脚本:

    import setuptools
    
    setuptools.setup(
        name="pytest-encode", # Replace with your own username
        version="0.0.1",
        author="hiyongz",
        author_email="zhiyo2016@163.com@example.com",
        description="set your encoding",
        long_description="show Chinese for your mark.parametrize().",
        url="https://github.com/pypa/sampleproject",
        project_urls={
            "Bug Tracker": "https://github.com/pypa/sampleproject/issues",
        },
        classifiers=[
            "Programming Language :: Python :: 3",
            "Framework :: Pytest",
            "Topic :: Software Develoment :: Testing",
        ],
        license='MIT License',
        packages=['pytest_encode'],
        keywords=[
            "pytest",
            "py.test",
            "pytest_encode",
        ],
        install_requires=[
            'pytest'
        ],
        python_requires=">=3.6",
        # 入口模块或者入口函数
        entry_points={
            'pytest11':[
                'pytest-encode = pytest_encode'
            ]
        },
           
        zip_safe=False,
    )
    
    1. 其中依赖包可以通过如下命令生成:
    $ pip freeze >requirements.txt
    
    1. entry_points为入口函数,使用pluggy插件中PluginManager类的load_setuptools_entrypoints方法加载,其中pytest11为入口点,这是官方定义的固定入口点,用于发现插件,参考https://docs.pytest.org/en/latest/how-to/writing_plugins.html

    __init__.py:

    import logging
    from typing import List
    
    import pytest
    
    logging.basicConfig(level=logging.INFO,
                        format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
                        datefmt='%a, %d %b %Y %H:%M:%S',
                        filename='report.log',
                        filemode='w'
                        )
    logger = logging.getLogger(__name__)
    
    def pytest_collection_modifyitems(session, config, items: List):
        for item in items:
            item.name = item.name.encode('utf-8').decode('unicode-escape')
            item._nodeid = item.nodeid.encode('utf-8').decode('unicode-escape')
        items.reverse()
    

    2、打包

    打包需要安装两个库:

    • wheel:pip install wheel
    • setuptools:pip install setuptools

    进入包目录(pytest-encode)下,执行目录:

    $ python setup.py sdist bdist_wheel
    

    命令执行完成后,新生成如下文件:

    pytest-encode/
    ├── build/
    	├── bdist.win-amd64
        └── lib
    		└── pytest_encode
    			└── __init__.py
    ├── dist/
    	├── pytest-encode-0.0.1.tar.gz
        └── pytest_encode-0.0.1-py3-none-any.whl		
    └──pytest_encode.egg-info/
    	├── dependency_links.txt
    	├── entry_points.txt
    	├── not-zip-safe
    	├── PKG-INFO
    	├── requires.txt
    	├── SOURCES.txt	
        └── test_encode.py
    

    pytest-encode-0.0.1.tar.gz为源码包,pytest_encode-0.0.1-py3-none-any.whl可以通过pip install命令安装

    3、测试打包文件

    我们新建一个Python虚拟环境,在新的虚拟环境下执行:

    $ pip list
    Package    Version
    ---------- -------
    pip        21.0.1
    setuptools 54.2.0
    

    只安装了两个包,下面安装刚才生成的包文件:

    $ pip install pytest_encode-0.0.1-py3-none-any.whl
    

    它会自动安装setup.py中定义的依赖包。

    然后在新的虚拟环境中编写测试用例并执行:

    import pytest
    from pytest_encode import logger
    
    @pytest.mark.parametrize("name",["张三","李四"])
    def test_name(name):
        logger.info("测试编码")
        print(name)
    
    $ pytest -vs test_encode.py::test_name
    

    结果(只截取部分):

    test_hook.py::test_name[李四] 李四
    PASSED
    test_hook.py::test_name[张三] 张三
    PASSED
    

    解码成功,并且生成了日志文件:
    report.log:

    Sat, 03 Apr 2021 20:21:45 test_encode.py[line:13] INFO 测试编码
    Sat, 03 Apr 2021 20:21:45 test_encode.py[line:13] INFO 测试编码
    

    表明安装的包生效了。

    4、发布包

    使用Twine来上传包到PyPI,需要注册一个PyPI账号

    然后安装twine:

    $ pip install twine
    

    上传:

    $ twine upload --repository test-encode dist/*
    

    上传过程中会让你输入前面注册的用户名和密码,上传成功后就可以在PyPI上查看

    参考资料

    1. https://docs.pytest.org/en/latest/how-to/writing_plugins.html
    2. https://docs.pytest.org/en/stable/reference.html?#hooks
    3. https://docs.pytest.org/en/stable/_modules/_pytest/hookspec.html#pytest_cmdline_parse
    4. https://github.com/pytest-dev/pytest/issues/3261
    5. https://packaging.python.org/tutorials/packaging-projects/
    6. pytest-ordering:https://github.com/ftobia/pytest-ordering
    7. pluggy:https://pluggy.readthedocs.io/en/latest/
    --THE END--

    欢迎关注公众号:「测试开发小记」及时接收最新技术文章!

  • 相关阅读:
    你人生中的那口井挖了没有?
    Stream接口
    console (控制台)
    assert.fail()
    assert.strictEqual()
    assert.equal()
    assert.ifError()
    assert.ok()
    nodejs assert 模块
    闭包
  • 原文地址:https://www.cnblogs.com/hiyong/p/14697298.html
Copyright © 2020-2023  润新知