pytest-xdist进程级并发过程及参数化说明--成都-阿木木

2023-05-16

关于分布式插件pytest-xdist进程级并发参数化说明

UI自动化脚本耗费时间较长,效率低下,我们该如何处理这种情况,提升测试效率,下面我会就分布式插件pytest-xdist的一个动态参数化问题,简单说明一下pytest-xdist的工作流程,以及数据收集过程,希望对大家有所帮助。

–成都-阿木木


说明:

​ 场景:我有100个UI自动化用例,假设每个用例执行时间为一分钟,那么我顺序执行需要执行100分钟,在敏捷开发模式下,耗时比较长,如果赶上项目上线,需要快速得到项目上线后的反馈,那么效率是非常低下的。使用pytest-xdist可以指定多个worker,进行执行用例,我指定10个worker,那么我整个测试脚本执行下来的理论时间为10分钟(PS:只是理论时间),大大的提升了效率。

分布式用例设计原则

  • 用例独立,独立运行
  • 用例没有顺序,随机运行
  • 用例重复运行,运行结束即清理
  • 用例的参数化使用非动态数据(重点:下面讲)

pytest-xdist安装

pip install pytest-xdist

pytest-xdist使用

pytest的命令行参数中指定-n x(运行的进程数量)

  • -n auto:可以自动获取系统的CPU核数,启用该参数CPU占用率会非常高,每个进程执行速度会非常慢,所以,worker越多,并不会按照理论时间来进行测试脚本运行效率的提升

  • -n x:手动指定CPU数量


pytest-xdist分布式执行用例参数化说明

这是一个测试路由器的case,使用了pytest.mark.parametrize进行参数生成,它的作用和ddt数据驱动框架一样,会执行两次测试用例,第一次传递参数“”,第二次传递一个随机10位的字符串参数

    @allure.story("Service")
    @allure.severity("normal")
    @allure.description("服务模块-路由器列表-新建路由器-序列号")
    @allure.testcase("XXX", name="测试用例位置")
    @allure.title("服务模块-路由器列表-新建路由器-序列号")
    @pytest.mark.parametrize("router_sn", ("", PubMethod.random_string("bnuiowehosdc235342", 10)))
    def test_617(self, login_page_class, service_page_class, function_driver, router_sn):
        logging.info("进入测试")
        login_page_class.mode_login_into_url()
        service_page_class.click_free_alert_close_icon()
        service_page_class.click_create_router_btn()
        service_page_class.send_keys_router_name(PubMethod.random_string("hfbudsio2353256", 10))
        service_page_class.send_keys_router_sn(router_sn)
        service_page_class.click_router_alert_determine_btn()
        tips_by_router_sn = service_page_class.get_tips_by_router_sn()
        if len(router_sn) == 0:
            AssertMethod.assert_equal_screen_shot(function_driver, (tips_by_router_sn, "请输入路由器序列号!"))
        else:
            AssertMethod.assert_equal_screen_shot(function_driver, (tips_by_router_sn, "请输入正确的路由器序列号!"))

这是pytest的命令行启动参数,我指定了-n参数为auto,运行run.py,框架路径:https://github.com/chineseluo/ui_auto_frame_v2,可以自己下载测试

    test_args = ['-s', '-q', '-n=auto', '--browser={}'.format(browser), '--browser_opt={}'.format(browser_opt),
                 '--type_driver={}'.format(type_driver)]
bringing up nodes...
[gw0] Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
[gw1] Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
[gw2] Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
[gw3] Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
[gw4] Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
[gw5] Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
[gw6] Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
[gw7] Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
gw0 [41] / gw1 [41] / gw2 [41] / gw3 [41] / gw4 [41] / gw5 [41] / gw6 [41] / gw7 [41]

scheduling tests via LoadScheduling

=================================== ERRORS ====================================
____________________________ ERROR collecting gw1 _____________________________
Different tests were collected between gw0 and gw1. The difference is:
--- gw0

+++ gw1

@@ -22,7 +22,7 @@

 TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_615
 TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_616
 TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_617[]
-TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_617[45u333isnn]
+TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_617[iw543ui332]
 TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_618_1
 TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_618_2
 TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_618_3
____________________________ ERROR collecting gw2 _____________________________
Different tests were collected between gw0 and gw2. The difference is:
--- gw0

+++ gw2

@@ -22,7 +22,7 @@

 TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_615
 TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_616
 TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_617[]
-TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_617[45u333isnn]
+TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_617[is4no2hw55]
 TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_618_1
 TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_618_2
 TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_618_3
____________________________ ERROR collecting gw3 _____________________________
Different tests were collected between gw0 and gw3. The difference is:
--- gw0

+++ gw3

可以看到我一用启用了8个worker,gw0~gw7,读取的系统默认CPU核数为8,gw0~gw1gw0~gw2gw0~gw3…以及其他几个worker报错:

Different tests were collected between gw0 and gw1. The difference is:--- gw0 +++ gw1

可以看到在test_617的两条case异常,有一个+号和减号,分析xdist用例收集,进程调度原理:
在这里插入图片描述
收集caseID及进程分发说明:根据简单的示意图可以看出,每一个worker(gwxxx)都会去进行一次标准caseID的收集,整个caseID的收集过程和pytest本身收集caseID的过程是一致的,每一个worker收集完caseID后发送给主节点(管理节点),主节点进行caseID的检查,包括了测试数据的检查,保证每个worker获取的case的数据是一致的,"generating tests twice must produce identical tests" 。在所有worker的测试用例集合相同的前提下,进行测试任务的分发,主节点下发的不再是caseID,而是测试case的索引,以方便告诉每个worker执行哪一个测试用例。
在这里插入图片描述
那么现在可以知道该异常出现的原因了,也就是说pytest-xdist在进行caseID收集的时候发现,参数化的case的入参不一致,我们是随机生成的,它只支持静态的有序的入参,下面我们修改一下脚本进行验证。

    @allure.story("Service")
    @allure.severity("normal")
    @allure.description("服务模块-路由器列表-新建路由器-序列号")
    @allure.testcase("XXX", name="测试用例位置")
    @allure.title("服务模块-路由器列表-新建路由器-序列号")
    def test_617_1(self, login_page_class, service_page_class, function_driver):
        logging.info("进入测试")
        login_page_class.mode_login_into_url()
        service_page_class.click_free_alert_close_icon()
        service_page_class.click_create_router_btn()
        service_page_class.send_keys_router_name(PubMethod.random_string("hfbudsio2353256", 10))
        service_page_class.send_keys_router_sn("")
        service_page_class.click_router_alert_determine_btn()
        tips_by_router_sn = service_page_class.get_tips_by_router_sn()
        AssertMethod.assert_equal_screen_shot(function_driver, (tips_by_router_sn, "请输入路由器序列号!"))

    @allure.story("Service")
    @allure.severity("normal")
    @allure.description("服务模块-路由器列表-新建路由器-序列号")
    @allure.testcase("XXX", name="测试用例位置")
    @allure.title("服务模块-路由器列表-新建路由器-序列号")
    def test_617_2(self, login_page_class, service_page_class, function_driver):
        logging.info("进入测试")
        login_page_class.mode_login_into_url()
        service_page_class.click_free_alert_close_icon()
        service_page_class.click_create_router_btn()
        service_page_class.send_keys_router_name(PubMethod.random_string("hfbudsio2353256", 10))
        service_page_class.send_keys_router_sn(PubMethod.random_string("bnuiowehosdc235342", 10))
        service_page_class.click_router_alert_determine_btn()
        AssertMethod.assert_equal_screen_shot(function_driver, (tips_by_router_sn, "请输入正确的路由器序列号!"))

检查日志,查看caseID的收集是否异常,可以看到下面的日志中,用例的ID收集无异常,验证通过。

bringing up nodes...
[gw0] Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
[gw1] Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
[gw2] Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
[gw3] Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
[gw4] Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
[gw5] Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
[gw6] Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
[gw7] Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
gw0 [41] / gw1 [41] / gw2 [41] / gw3 [41] / gw4 [41] / gw5 [41] / gw6 [41] / gw7 [41]

scheduling tests via LoadScheduling

TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_606_1 
TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_605_1_and_2[普通用户] 
TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_606_2 
TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_606_4 
TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_606_3 
TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_606_5 
TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_605_1_and_2[管理员] 
TestCases/Service/test_servicePageCase.py::TestServicePageCase::test_606_6 

总结:

通过分析pytest-xdist的参数化异常原因,可以了解到pytest-xdist的工作流程原理,针对性的处理异常,如果需要使用到pytest-xdist插件的小伙伴,可以看一下,做一下测试,进行简单验证加深了解。
欢迎加入测试交流群:夜行者自动化测试(816489363)进行交流学习QAQ

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

pytest-xdist进程级并发过程及参数化说明--成都-阿木木 的相关文章

  • 使用 py.test 在 Python 中测试正则表达式

    正则表达式对我来说仍然是一门黑暗艺术 但我认为这是需要练习的事情之一 因此 我更关心能够生成 py test 函数来显示我的正则表达式失败的地方 我当前的代码是这样的 my regex re compile
  • pytest django:无法访问固定装置拆卸中的数据库

    我需要在使用夹具后显式删除它 我知道 pytest django 默认情况下会在拆卸时删除所有对象 但在这种特殊情况下 我需要手动执行此操作 然而 虽然我的测试被标记为pytest mark django db 我能够创建一个夹具 但在经过
  • pytest:无法模拟我的班级的 __init__

    我有一个习惯Db类 具有基本操作 我正在尝试围绕它编写测试 在 init 在我的班级中 我正在连接到我想避免的实际数据库 因为我只是编写单元测试 不需要连接到实际数据库 这是我的代码 mock patch mydb Db pytest ma
  • 如何使用 pytest 测试无限 while 循环

    我目前正在编写一个与bamboo 构建服务器交互的小库 测试是使用 pytest 完成的 我陷入了以下问题 我想测试一个 while 循环 该循环运行直到满足某些状态 阅读 pytest 文档 我尝试 模拟 monkeypatch 状态 但
  • 在 Flask-SQLAlchemy 中隔离 py.test 数据库会话

    我正在尝试使用 Flask SQLAlchemy 构建 Flask 应用程序 我使用 pytest 来测试数据库 问题之一似乎是在不同测试之间创建隔离的数据库会话 我编写了一个最小的完整示例来突出问题 请注意test user schema
  • 您将如何对这个 SQLAlchemy Core 查询/函数进行单元测试?

    我正在努力学习如何正确进行单元测试 鉴于这个功能 def get user details req user id users sa Table users db metadata autoload True s sa select use
  • 在 py.test 中的每个测试之前和之后运行代码?

    我想在测试套件中的每个测试之前和之后运行额外的设置和拆卸检查 我看过固定装置 但不确定它们是否是正确的方法 我需要在每次测试之前运行设置代码 并且需要在每次测试后运行拆卸检查 我的用例是检查未正确清理的代码 它会留下临时文件 在我的设置中
  • 使用 pytest 在 VSCODE 中设置 python 测试时出现问题

    我正在尝试将 VSCode 中的测试扩展与 Python 扩展一起使用 我使用 pytest 作为我的测试库 我的文件夹结构如下所示 PACKAGENAME PACKAGENAME init py main py tests test ma
  • 在 pytest 测试中记录日志

    我想在测试函数中放置一些日志语句来检查一些状态变量 我有以下代码片段 import pytest os import logging logging basicConfig level logging DEBUG mylogger logg
  • 如何在 pytest 中仅运行未标记的测试

    我的 python 测试代码中有几个标记 pytest mark slowtest pytest mark webtest pytest mark stagingtest 我可以使用标记有选择地运行测试 例如pytest m slowtes
  • 如何使用 django-pytest 跟踪 Django 重定向?

    在设置一个档案索引视图 https docs djangoproject com en 2 0 ref class based views generic date based django views generic dates Arch
  • 使用 pytest 生成 csv 文件报告

    是否可以以某种方式在 csv 文件中生成测试执行报告 我使用 python selenium pytest 任何建议将不胜感激 我写了一个pytest csv https github com nicoulaj pytest csv插件 希
  • py.test 无法导入我的模块

    我正在努力正确导入 python 我想要实现的是拥有一个包含多个源文件的模块和一个包含单元测试的测试文件夹 无论我做什么 我都无法让 py test 3 执行我的测试 我的目录布局如下所示 module init py testclass
  • Pytest - 测试解析器错误:无法识别的参数

    我正在尝试测试一个非常简单的函数 由于多次尝试测试使用参数解析器作为参数的更复杂的函数而失败 来自 runfile py import argparse import os def get input args parser argpars
  • 在 pytest 中参数化并运行单个测试

    如何从配置了参数化的集合中运行单个测试 假设我有以下测试方法 pytest mark parametrize PARAMETERS LIST PARAMETERS VALUES def test my feature self param1
  • pytest - ModuleNotFoundError - python 3.6.4

    我有一个具有以下布局的项目 MANIFEST in README md init py company init py api init py auth py debug py exceptions py reporting py rest
  • 如何在 pytest 中测试类层次结构?

    我已经使用 pytest 一段时间了 并学会了喜欢参数化和固定装置 我第一次想测试一些具有分支继承结构的类 当然 我想为子类重用测试用例 假设我有以下包结构 mock pkg child py grandchild py parent py
  • 如何从视图中删除单元测试的“@oidc.login_required”?

    I use 烧瓶样机 questions tagged flask oidc用于用户登录和pytest questions tagged pytest供测试用 对于单元测试 我想 删除 oidc require login 我怎样才能做到这
  • 当日志在不同进程中发出时,caplog 中的消息为空

    我正在使用 log cli true 运行测试 剧本 import logging import sys from multiprocessing import Process logging basicConfig stream sys
  • 将固定装置传递给 pytest 中的测试类

    考虑以下伪代码来演示我的问题 import pytest pytest fixture def param1 return smth yield wilma pytest fixture def param2 return smth yie

随机推荐