百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

软件测试/测试开发丨利用 pytest 玩转数据驱动测试框架

ccwgpt 2024-11-02 11:03 32 浏览 0 评论

公众号搜索:TestingStudio 霍格沃兹测试开发的干货都很硬核

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>

可以从 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 的 parent 就是 Package:

<Module test_parse.py>:

  parent: <Package tests>

注意大小写: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):
    # 用于创建测试用例
    @classmethod
    def from_parent(cls, parent, **kw):
        """The public constructor."""
        return super().from_parent(parent=parent, **kw)
    # 获取实例
    def _getobj(self):
        assert self.parent is not None
        return getattr(self.parent.obj, self.originalname)  # type: ignore[attr-defined]
    # 运行测试用例
    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?

手动构建 item 就是模拟 pytest 构建 Function 的过程。也就是说,需要创建 Config ,然后利用 Config 创建 Session ,然后利用 Session 创建 Package ,…,最后创建 Function。

其实没这么复杂, 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)
        # 返回自已定义的 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:
        # 关键代码:从路径导入 module
        mod = import_path(self.fspath, mode=importmode) 
    except SyntaxError as e:
        raise self.CollectError(
            ExceptionInfo.from_current().getrepr(style="short")
        ) from e
        # 省略部分代码...

但是,如果使用数据驱动,即用户创建的数据文件 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
# 动态创建 module
module = types.ModuleType(name)
def function_template(*args, **kwargs):
    print(123)
# 向 module 中加入函数
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)
        # 动态创建 module
        module = types.ModuleType(path.purebasename)
        def function_template(*args, **kwargs):
            print(123)
        # 向 module 中加入函数
        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)
        # 动态创建 module
        module = types.ModuleType(path.purebasename)
        # 解析 yaml 内容
        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):
                    """
                    函数模块
                    """
                    # 遍历多个测试步骤 [print: 123, print: 456]
                    for step_dic in steps:
                        # 解析一个测试步骤 print: 123
                        for step_key, step_value in step_dic.items():
                            if step_key == "print":
                                print(step_value)


                # 向 module 中加入函数
                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创建数据驱动测试框架就介绍到这里啦,希望能给大家带来一定的帮助。大家有什么不懂的地方或者有疑惑也可以留言讨论哈,让我们共同进步呦!

相关推荐

一个基于.Net Core遵循Clean Architecture原则开源架构

今天给大家推荐一个遵循CleanArchitecture原则开源架构。项目简介这是基于Asp.netCore6开发的,遵循CleanArchitecture原则,可以高效、快速地构建基于Ra...

AI写代码翻车无数次,我发现只要提前做好这3步,bug立减80%

写十万行全是bug之后终于找到方法了开发"提示词管理助手"新版本那会儿,我差点被bug整崩溃。刚开始两周,全靠AI改代码架构,结果十万行程序漏洞百出。本来以为AI说没问题就稳了,结果...

OneCode低代码平台的事件驱动设计:架构解析与实践

引言:低代码平台的事件驱动范式在现代软件开发中,事件驱动架构(EDA)已成为构建灵活、松耦合系统的核心范式。OneCode低代码平台通过创新性的注解驱动设计,将事件驱动理念深度融入平台架构,实现了业务...

国内大厂AI插件评测:根据UI图生成Vue前端代码

在IDEA中安装大厂的AI插件,打开ruoyi增强项目:yudao-ui-admin-vue31.CodeBuddy插件登录腾讯的CodeBuddy后,大模型选择deepseek-v3,输入提示语:...

AI+低代码技术揭秘(二):核心架构

本文档介绍了为VTJ低代码平台提供支持的基本架构组件,包括Engine编排层、Provider服务系统、数据模型和代码生成管道。有关UI组件库和widget系统的信息,请参阅UI...

GitDiagram用AI把代码库变成可视化架构图

这是一个名为gitdiagram的开源工具,可将GitHub仓库实时转换为交互式架构图,帮助开发者快速理解代码结构。核心功能一键可视化:替换GitHubURL中的"hub...

30天自制操作系统:第六天:代码架构整理与中断处理

1.拆开bootpack.c文件。根据设计模式将对应的功能封装成独立的文件。2.初始化pic:pic(可编程中断控制器):在设计上,cpu单独只能处理一个中断。而pic是将8个中断信号集合成一个中断...

AI写代码越帮越忙?2025年研究揭露惊人真相

近年来,AI工具如雨后春笋般涌现,许多人开始幻想程序员的未来就是“对着AI说几句话”,就能轻松写出完美的代码。然而,2025年的一项最新研究却颠覆了这一期待,揭示了一个令人意外的结果。研究邀请了16位...

一键理解开源项目:两个自动生成GitHub代码架构图与说明书工具

一、GitDiagram可以一键生成github代码仓库的架构图如果想要可视化github开源项目:https://github.com/luler/reflex_ai_fast,也可以直接把域名替换...

5分钟掌握 c# 网络通讯架构及代码示例

以下是C#网络通讯架构的核心要点及代码示例,按协议类型分类整理:一、TCP协议(可靠连接)1.同步通信//服务器端usingSystem.Net.Sockets;usingTcpListene...

从复杂到优雅:用建造者和责任链重塑代码架构

引用设计模式是软件开发中的重要工具,它为解决常见问题提供了标准化的解决方案,提高了代码的可维护性和可扩展性,提升了开发效率,促进了团队协作,提高了软件质量,并帮助开发者更好地适应需求变化。通过学习和应...

低代码开发当道,我还需要学习LangChain这些框架吗?| IT杂谈

专注LLM深度应用,关注我不迷路前两天有位兄弟问了个问题:当然我很能理解这位朋友的担忧:期望效率最大化,时间用在刀刃上,“不要重新发明轮子”嘛。铺天盖地的AI信息轰炸与概念炒作,很容易让人浮躁与迷茫。...

框架设计并不是简单粗暴地写代码,而是要先弄清逻辑

3.框架设计3.框架设计本节我们要开发一个UI框架,底层以白鹭引擎为例。框架设计的第一步并不是直接撸代码,而是先想清楚设计思想,抽象。一个一个的UI窗口是独立的吗?不是的,...

大佬用 Avalonia 框架开发的 C# 代码 IDE

AvalonStudioAvalonStudio是一个开源的跨平台的开发编辑器(IDE),AvalonStudio的目标是成为一个功能齐全,并且可以让开发者快速使用的IDE,提高开发的生产力。A...

轻量级框架Lagent 仅需20行代码即可构建自己的智能代理

站长之家(ChinaZ.com)8月30日消息:Lagent是一个专注于基于LLM模型的代理开发的轻量级框架。它的设计旨在简化和提高这种模型下代理的开发效率。LLM模型是一种强大的工具,可以...

取消回复欢迎 发表评论: