利用 pytest 玩转数据驱动测试框架

本文选自测试人社区

pytest架构是什么?

首先,来看一个 pytest 的例子:

        def test_a():
              print(123)
        collected 1 item
            test_a.py .                                                                                                            [100%]
                ============ 1 passed in 0.02s =======================

输出结果很简单:收集到 1 个用例,并且这条测试用例执行通过。

此时思考两个问题:

  1. pytest 如何收集到用例的?
  2. pytest 如何把 python 代码,转换成 pytest 测试用例(又称 item) ?

pytest如何做到收集到用例的?

这个很简单,遍历执行目录,如果发现目录的模块中存在符合” pytest 测试用例要求的 python 对象”,就将之转换为 pytest 测试用例。

比如编写以下 hook 函数:

        def pytest_collect_file(path, parent):
                print("hello", path)
        hello C:\Users\yuruo\Desktop\tmp\tmp123\tmp\testcase\__init__.py
            hello C:\Users\yuruo\Desktop\tmp\tmp123\tmp\testcase\conftest.py
                hello C:\Users\yuruo\Desktop\tmp\tmp123\tmp\testcase\test_a.py

会看到所有文件内容。

如何构造pytest的item?

pytest 像是包装盒,将 python 对象包裹起来,比如下图:

当写好 python 代码时:

        def test_a:
                print(123)

会被包裹成 Function :

        <function test_a>
</function>

可以从 hook 函数中查看细节:

        def pytest_collection_modifyitems(session, config, items):
                pass

于是,理解包裹过程就是解开迷题的关键。pytest 是如何包裹 python 对象的?

下面代码只有两行,看似简单,但暗藏玄机!

        def test_a:
                print(123)

把代码位置截个图,如下:

我们可以说,上述代码是处于”testcase包”下的 “test_a.py模块”的”test_a函数”, pytest 生成的测试用例也要有这些信息:

处于”testcase包”下的 “test_a.py模块”的”test_a测试用例:

把上述表达转换成下图:

pytest 使用 parent 属性表示上图层级关系,比如 Module 是 Function 的上级, Function 的 parent 属性如下:

        <function test_a>:
              parent: <module test_parse.py>
</module></function>

当然 Module 的 parent 就是 Package:

        <module test_parse.py>:
              parent: <package tests>
</package></module>

注意大小写:Module 是 pytest 的类,用于包裹 python 的 module 。Module 和 module 表示不同意义。

这里科普一下,python 的 package 和 module 都是真实存在的对象,你可以从 obj 属性中看到,比如 Module 的 obj
属性如下:

如果理解了 pytest 的包裹用途,非常好!我们进行下一步讨论:如何构造 pytest 的 item ?

以下面代码为例:

        def test_a:
                print(123)

构造 pytest 的 item ,需要:

  1. 构建 Package
  2. 构建 Module
  3. 构建 Function
    以构建 Function 为例,需要调用其 from_parent()方法进行构建,其过程如下图:

从函数名 from_parent,就可以猜测出,”构建 Function”一定与其 parent 有不小联系!又因为 Function 的 parent
是 Module :根据下面 Function 的部分代码(位于 python.py 文件):

        class Function(PyobjMixin, nodes.Item):
                # &#x7528;&#x4E8E;&#x521B;&#x5EFA;&#x6D4B;&#x8BD5;&#x7528;&#x4F8B;
                        @classmethod
                                def from_parent(cls, parent, **kw):
                                            """The public constructor."""
                                                        return super().from_parent(parent=parent, **kw)
                                                                # &#x83B7;&#x53D6;&#x5B9E;&#x4F8B;
                                                                        def _getobj(self):
                                                                                    assert self.parent is not None
                                                                                                return getattr(self.parent.obj, self.originalname)  # type: ignore[attr-defined]
                                                                                                        # &#x8FD0;&#x884C;&#x6D4B;&#x8BD5;&#x7528;&#x4F8B;
                                                                                                                def runtest(self) -> None:
                                                                                                                            """Execute the underlying test function."""
                                                                                                                                        self.ihook.pytest_pyfunc_call(pyfuncitem=self)

得出结论,可以利用 Module 构建 Function!其调用伪代码如下:

        Function.from_parent(Module)

既然可以利用 Module 构建 Function, 那如何构建 Module ?

当然是利用 Package 构建 Module!

        Module.from_parent(Package)

既然可以利用 Package 构建 Module 那如何构建 Package ?

别问了,快成套娃了,请看下图调用关系:

pytest 从 Config 开始,层层构建,直到 Function !Function 是 pytest 的最小执行单元。
如何手动构建item?

  &#x624B;&#x52A8;&#x6784;&#x5EFA; item &#x5C31;&#x662F;&#x6A21;&#x62DF; pytest &#x6784;&#x5EFA; Function &#x7684;&#x8FC7;&#x7A0B;&#x3002;&#x4E5F;&#x5C31;&#x662F;&#x8BF4;&#xFF0C;&#x9700;&#x8981;&#x521B;&#x5EFA; Config &#xFF0C;&#x7136;&#x540E;&#x5229;&#x7528; Config &#x521B;&#x5EFA; Session
  &#xFF0C;&#x7136;&#x540E;&#x5229;&#x7528; Session &#x521B;&#x5EFA; Package &#xFF0C;&#x2026;&#xFF0C;&#x6700;&#x540E;&#x521B;&#x5EFA; Function&#x3002;

其实没这么复杂, pytest 会自动创建好 Config, Session和 Package ,这三者不用手动创建。

比如编写以下 hook 代码,打断点查看其 parent 参数:

        def pytest_collect_file(path, parent):
                pass

如果遍历的路径是某个包(可从path参数中查看具体路径),比如下图的包:

其 parent 参数就是 Package ,此时可以利用这个 Package 创建 Module :

编写如下代码即可构建 pytest 的 Module ,如果发现是 yaml 文件,就根据 yaml 文件内容动态创建 Module 和 module :

        from _pytest.python import Module, Package
            def pytest_collect_file(path, parent):
                    if path.ext == ".yaml":
                                pytest_module = Module.from_parent(parent, fspath=path)
                                            # &#x8FD4;&#x56DE;&#x81EA;&#x5DF2;&#x5B9A;&#x4E49;&#x7684; python module
                                                        pytest_module._getobj = lambda : MyModule
                                                                    return pytest_module

需要注意,上面代码利用猴子补丁改写了 _getobj 方法,为什么这么做?

Module 利用 _getobj 方法寻找并导入(import语句) path 包下的 module ,其源码如下:

        # _pytest/python.py Module
            class Module(nodes.File, PyCollector):
                    def _getobj(self):
                                return self._importtestmodule()
                                    def _importtestmodule(self):
                                            # We assume we are only called once per module.

                                                    importmode = self.config.getoption("--import-mode")
                                                            try:
                                                                        # &#x5173;&#x952E;&#x4EE3;&#x7801;&#xFF1A;&#x4ECE;&#x8DEF;&#x5F84;&#x5BFC;&#x5165; module
                                                                                    mod = import_path(self.fspath, mode=importmode)
                                                                                            except SyntaxError as e:
                                                                                                        raise self.CollectError(
                                                                                                                        ExceptionInfo.from_current().getrepr(style="short")
                                                                                                                                    ) from e
                                                                                                                                                # &#x7701;&#x7565;&#x90E8;&#x5206;&#x4EE3;&#x7801;...

但是,如果使用数据驱动,即用户创建的数据文件 test_parse.yaml ,它不是 .py 文件,不会被 python 识别成 module (只有
.py 文件才能被识别成 module)。

这时,就不能让 pytest 导入(import语句) test_parse.yaml ,需要动态改写 _getobj ,返回自定义的 module !

因此,可以借助 lambda 表达式返回自定义的 module :

        lambda : MyModule

如何自定义module

这就涉及元编程技术:动态构建 python 的 module ,并向 module 中动态加入类或者函数:

        import types
            # &#x52A8;&#x6001;&#x521B;&#x5EFA; module
                module = types.ModuleType(name)
                    def function_template(*args, **kwargs):
                            print(123)
                                # &#x5411; module &#x4E2D;&#x52A0;&#x5165;&#x51FD;&#x6570;
                                    setattr(module, "test_abc", function_template)

综上,将自己定义的 module 放入 pytest 的 Module 中即可生成 item :

        # conftest.py
            import types
                from _pytest.python import Module
                    def pytest_collect_file(path, parent):
                            if path.ext == ".yaml":
                                        pytest_module = Module.from_parent(parent, fspath=path)
                                                    # &#x52A8;&#x6001;&#x521B;&#x5EFA; module
                                                                module = types.ModuleType(path.purebasename)
                                                                            def function_template(*args, **kwargs):
                                                                                            print(123)
                                                                                                        # &#x5411; module &#x4E2D;&#x52A0;&#x5165;&#x51FD;&#x6570;
                                                                                                                    setattr(module, "test_abc", function_template)
                                                                                                                                pytest_module._getobj = lambda: module
                                                                                                                                            return pytest_module

创建一个 yaml 文件,使用 pytest 运行:

        ======= test session starts ====
            platform win32 -- Python 3.8.1, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
                rootdir: C:\Users\yuruo\Desktop\tmp
                    plugins: allure-pytest-2.8.11, forked-1.3.0, rerunfailures-9.1.1, timeout-1.4.2, xdist-2.2.1
                        collected 1 item
                            test_a.yaml 123
                                .
                                    ======= 1 passed in 0.02s =====
                                        PS C:\Users\yuruo\Desktop\tmp>

现在停下来,回顾一下,我们做了什么?

借用 pytest hook ,将 .yaml 文件转换成 python module。

作为一个数据驱动测试框架,我们没做什么?

没有解析 yaml 文件内容!上述生成的 module ,其内的函数如下:

        def function_template(*args, **kwargs):
                print(123)

只是简单打印 123 。数据驱动测试框架需要解析 yaml 内容,根据内容动态生成函数或类。比如下面 yaml 内容:

        test_abc:
              - print: 123

表达的含义是”定义函数 test_abc,该函数打印 123″。

注意:关键字含义应该由你决定,这里仅给一个 demo 演示!

可以利用 yaml.safe_load 加载 yaml 内容,并进行关键字解析,其中 path.strpath代表 yaml 文件的地址:

        import types
            import yaml
                from _pytest.python import Module
                    def pytest_collect_file(path, parent):
                            if path.ext == ".yaml":
                                        pytest_module = Module.from_parent(parent, fspath=path)
                                                    # &#x52A8;&#x6001;&#x521B;&#x5EFA; module
                                                                module = types.ModuleType(path.purebasename)
                                                                            # &#x89E3;&#x6790; yaml &#x5185;&#x5BB9;
                                                                                        with open(path.strpath) as f:
                                                                                                        yam_content = yaml.safe_load(f)
                                                                                                                        for function_name, steps in yam_content.items():

                                                                                                                                                        def function_template(*args, **kwargs):
"""
                                                                                                                                                                                                        &#x51FD;&#x6570;&#x6A21;&#x5757;
"""
                                                                                                                                                                                                                                                        # &#x904D;&#x5386;&#x591A;&#x4E2A;&#x6D4B;&#x8BD5;&#x6B65;&#x9AA4; [print: 123, print: 456]
                                                                                                                                                                                                                                                                                for step_dic in steps:
                                                                                                                                                                                                                                                                                                            # &#x89E3;&#x6790;&#x4E00;&#x4E2A;&#x6D4B;&#x8BD5;&#x6B65;&#x9AA4; print: 123
                                                                                                                                                                                                                                                                                                                                        for step_key, step_value in step_dic.items():
                                                                                                                                                                                                                                                                                                                                                                        if step_key == "print":
                                                                                                                                                                                                                                                                                                                                                                                                            print(step_value)

                                                                                                                                                                                                                                                                                                                                                                                                                                            # &#x5411; module &#x4E2D;&#x52A0;&#x5165;&#x51FD;&#x6570;
                                                                                                                                                                                                                                                                                                                                                                                                                                                                setattr(module, function_name, function_template)
                                                                                                                                                                                                                                                                                                                                                                                                                                                                            pytest_module._getobj = lambda: module
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        return pytest_module

上述测试用例运行结果如下:

        === test session starts ===
            platform win32 -- Python 3.8.1, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
                rootdir: C:\Users\yuruo\Desktop\tmp
                    plugins: allure-pytest-2.8.11, forked-1.3.0, rerunfailures-9.1.1, timeout-1.4.2, xdist-2.2.1
                        collected 1 item
                            test_a.yaml 123
                                .
                                    === 1 passed in 0.02s ====

当然,也支持复杂一些的测试用例:

        test_abc:
              - print: 123
              -       - print: 456
              -     test_abd:
              -       - print: 123
              -       - print: 456

其结果如下:

        == test session starts ==
            platform win32 -- Python 3.8.1, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
                rootdir: C:\Users\yuruo\Desktop\tmp
                    plugins: allure-pytest-2.8.11, forked-1.3.0, rerunfailures-9.1.1, timeout-1.4.2, xdist-2.2.1
                        collected 2 items
                            test_a.yaml 123
                                456
                                    .123
                                        456
                                            .
                                                == 2 passed in 0.02s ==

利用pytest创建数据驱动测试框架就介绍到这里啦,希望能给大家带来一定的帮助。大家有什么不懂的地方或者有疑惑也可以留言讨论哈,让我们共同进步呦!

** _
来霍格沃兹测试开发学社,学习更多软件测试与测试开发的进阶技术,知识点涵盖web自动化测试 app自动化测试、接口自动化测试、测试框架、性能测试、安全测试、持续集成/持续交付/DevOps,测试左移、测试右移、精准测试、测试平台开发、测试管理等内容,课程技术涵盖bash、pytest、junit、selenium、appium、postman、requests、httprunner、jmeter、jenkins、docker、k8s、elk、sonarqube、jacoco、jvm-sandbox等相关技术,全面提升测试开发工程师的技术实力
QQ交流群:484590337
公众号 TestingStudio
视频资料领取:https://qrcode.testing-studio.com/f?from=CSDN&url=https://ceshiren.com/t/topic/15844
点击查看更多信息

利用 pytest 玩转数据驱动测试框架

Original: https://blog.csdn.net/weixin_46635091/article/details/122211779
Author: 霍老湿
Title: 利用 pytest 玩转数据驱动测试框架

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/772800/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 嵌入式分享合集117

    一、获取STM32代码运行时间的技巧 测试代码的运行时间的两种方法: 使用单片机内部定时器,在待测程序段的开始启动定时器,在待测程序段的结尾关闭定时器。为了测量的准确性,要进行多次…

    Python 2023年10月11日
    038
  • Django项目实战——用户投票系统(四)

    Django项目实战——用户投票系统4 Django项目实战——用户投票系统(四) * 前情概要 正文开始 – 编写一个简单的表单 简化代码——使用通用视图 + 改良U…

    Python 2023年8月6日
    045
  • Xxl-job的使用

    官方文档地址 项目开发中,常常以下场景需要分布式任务调度:1、同一服务多个实例的任务存在互斥时,需要统一协调2、定时任务的执行需要支持高可用、监控运维、故障告警3、需要统一管理和追…

    Python 2023年10月1日
    093
  • 网易易盾云图片(行为)验证码,网易验证码,前端vue后端python drf。前后分离

    网易易盾开通 官网相关链接:网易易盾在线体验:在线体验验证码 联系客服开通体验账户后,点击进入管理中心 添加产品,添加时:测试先随意起个名字,选择web。准备开工 ; 前端 Vue…

    Python 2023年8月6日
    056
  • 计算机网络(Learning Records)

    背景:没想到本专业并不开设这门课程,感觉过于逆天,之前开发的时候了解过相关知识但是从来没有系统地学过,就自己看了书,总结一下参考:《TCP/IP详解 卷1:协议》 概述 大多数网络…

    Python 2023年10月19日
    032
  • Go语学习笔记 – 实现将mp4通过rtmp推送流媒体服务

    目录 前言 环境 代码 总结 前言 最近研究golang音视频处理的时候,试着将mp4推送流媒体服务。下面分享一下代码,大家有需要可以研究一下,按照自己的需求改改。 环境 需要安装…

    Python 2023年10月11日
    057
  • 读书方法

    旧书不厌百回读,熟读深知子自知。 要研究透彻本专业的经典。 读书要学会终身下苦功夫,知十而非真知,不如知一之真知也,读书不能一知半解。 读书决定了一个人的见识和思想的深刻,这些都是…

    Python 2023年10月25日
    034
  • 《利用Python进行数据分析》第七章——数据清洗与准备2

    在经过上文章对数据的缺失值进行过滤和补全,下面讲数据的一些转换,主要讲解数据的重新排列、过滤以及其他转换是另外一系列重要的操作。 1.1 删除重复值 当DataFrame出现重复行…

    Python 2023年8月18日
    042
  • 初识数据分析01——Pandas入门

    导入 Pandas 与 NumPy: In [1]: import numpy as np In [2]: import pandas as pd 载入数据 pd.read_csv…

    Python 2023年8月9日
    032
  • pygmae游戏开发——像素鸟(flappy_bird)

    一、框架建立: 1.导入模块: import sys,pygame,time,os,randomfrom pygame.locals import * 在下方package处搜索添…

    Python 2023年9月21日
    050
  • python:np.reshape(a,b,c,d)

    np.reshape(n,m)重新生成n*m维的数组 如:reshape(2,-1) -1表示0轴分成2维后,1轴按元素个数分,不指定维度 1、 # 3页 2*2 import n…

    Python 2023年8月29日
    043
  • 我的近况

    作者:皎然CEO 链接:https://www.cnblogs.com/jiaoran/p/now.html 个性签名:独学而无友,则孤陋而寡闻。做一个灵魂有趣的人! 如果觉得这篇…

    Python 2023年6月12日
    054
  • Ubuntu安装配置pip3及numpy库

    Ubuntu安装配置pip3及numpy库 1.安装pip3 2.配置pip3 3.安装NumPy库 1.安装pip3 (1)输入pip3的安装命令 sudo apt instal…

    Python 2023年8月23日
    045
  • 梳理selenium的鼠标方法

    你需要有一定的selenium基础 基本用法 包导入 from selenium.webdriver import ActionChains 调用方式一:链式调用 ActionCh…

    Python 2023年6月12日
    050
  • 基于Echarts构建大数据招聘岗位数据可视化大屏

    🤵‍♂️ 个人主页:@艾派森的个人主页✍🏻作者简介:Python学习者🐋 希望大家多多支持,我们一起进步!😄如果文章对你有帮助的话,欢迎评论 💬点赞👍🏻 收藏 📂加关注+ 目录 1…

    Python 2023年11月8日
    044
  • 利用pytest hook函数实现自动化测试结果推送企业微信

    前言 通常,自动化测试用例在执行完成后,都会发送一个结果,以通知测试人员或测试leader测试的结果。如有测试失败的情况,测试人员再去查看具体的测试报告,检查是哪个场景没有测试通过…

    Python 2023年9月10日
    035
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球