前言:

  • 实际项目中的用例数量会非常多,几百上千;如果采用单进程串行执行的话会非常耗费时间。假设每条用例耗时2s,1000条就需要2000s $\approx$ 33min;还要加上用例加载、测试前/后置套件等耗时;导致测试执行效率会相对低。
  • 想象一下如果开发改动一块代码,我们需要回归一下,这时候执行一下自动化用例需要花费大半个小时或者好几个小时的时间,这是我们无法容忍的。
  • 为了节省项目测试时间,需要多个测试用例同时并行执行;这就是一种分布式场景来缩短测试用例的执行时间,提高效率。

分布式执行用例的原则

  • 用例之间是相互独立的,没有依赖关系,完全可以独立运行;
  • 用例执行没有顺序要求,随机顺序都能正常执行;
  • 每个用例都能重复运行,运行结果不会影响其他用例。

项目结构



测试脚本

# test1/test_1.py
import time def test1_test1():
time.sleep(1)
assert 1 == 1, "1==1" def test1_test2():
time.sleep(1)
assert 1 == 1, "1==1" class TestDemo1:
def test_inner_1(self):
time.sleep(1)
assert 1 == 1, "1==1" class TestDemo2:
def test_inner_2(self):
time.sleep(1)
assert 1 == 1, "1==1"
# test1/inner/test_3.py
import time def test3_test1():
time.sleep(1)
assert 1 == 1, "1==1" def test3_test2():
time.sleep(1)
assert 1 == 1, "1==1" # test2/test_2.py
import time def test2_test1():
time.sleep(1)
assert 1 == 1, "1==1" def test2_test2():
time.sleep(1)
assert 1 == 1, "1==1" # test2/inner/test_3.py
import time def test4_test1():
time.sleep(1)
assert 1 == 1, "1==1" def test4_test2():
time.sleep(1)
assert 1 == 1, "1==1"

正常执行:需要8.10s

多进程执行用例之 pytest-xdist

多cpu并行执行用例,直接加个-n参数即可,后面num参数就是并行数量,比如num设置为3

pytest -v -n num

参数:

  1. -n auto : 自动侦测系统里的CPU数目
  2. -n num : 指定运行测试的处理器进程数

多进程并行执行:耗时2.66s大大的缩短了测试用例的执行时间。

pytest-xdist分布式测试的原理:

  1. xdist的分布式类似于一主多从的结构,master负责下发命令,控制slave;slave根据master的命令执行特定测试任务。

  2. 在xdist中,主是master,从是workers;xdist会产生一个或多个workers,workers都通过master来控制,每个worker相当于一个mini版pytest执行器

  3. master不执行测试任务,只对worker收集到的所有用例进行分发;每个worker负责执行测试用例,然后将执行结果反馈给master;由master统计最终测试结果。

pytest-xdist分布式测试的流程:

第一步:master创建worker

  1. master在测试会话(test session)开始前产生一个或多个worker。

  2. master和worker之间是通过execnet网关来通信的。

  3. 实际编译执行测试代码的worker可能是本地机器也可能是远程机器。

第二步:workers收集测试项用例

  1. 每个worker类似一个迷你型的pytest执行器

  2. worker会执行一个完整的test collection过程。【收集所有测试用例的过程】

  3. 然后把测试用例的ids返回给master。【ids表示收集到的测试用例路径】

  4. master不执行任何测试用例。

注意:分布式测试(pytest-xdist)方式执行测试时不会输出测试用例中的print内容,因为master并不执行测试用例。

第三步:master检测workers收集到的测试用例集

  1. master接收到所有worker收集的测试用例集之后,master会进行一些完整性检查,以确保所有worker都收集到一样的测试用例集(包括顺序)。

  2. 如果检查通过,会将测试用例的ids列表转换成简单的索引列表,每个索引对应一个测试用例的在原来测试集中的位置。

  3. 这个方案可行的原因是:所有的节点都保存着相同的测试用例集。

  4. 并且使用这种方式可以节省带宽,因为master只需要告知workers需要执行的测试用例对应的索引,而不用告知完整的测试用例信息。

第四步:master分发测试用例

有以下四种分发策略:命令行参数 --dist=mode选项(默认load)

  • each:master将完整的测试索引列表分发到每个worker,即每个worker都会执行一遍所有的用例。

  • load:master将大约$\frac{1}{n}$的测试用例以轮询的方式分发到各个worker,剩余的测试用例则会等待worker执行完测试用例以后再分发;每个用例只会被其中一个worker执行一次。

  • loadfile:master分发用例的策略为按ids中的文件名(test_xx.py/xx_test.py)进行分发,即同一个测试文件中的测试用例只会分发给其中一个worker;具有一定的隔离性。

  • loadscope:master分发用例对策略为按作用域进行分发,同一个模块下的测试函数或某个测试类中的测试函数会分发给同一个worker来执行;即py文件中无测试类的话(只有测试function)将该模块分发给同一个worker执行,如果有测试类则会将该文件中的测试类只会分发给同一个worker执行,多个类可能分发给多个worker;目前无法自定义分组,按类 class 分组优先于按模块 module 分组。

注意:可以使用pytest_xdist_make_scheduler这个hook来实现自定义测试分发逻辑。

如:想按目录级别来分发测试用例:

from xdist.scheduler import LoadScopeScheduling

class CustomizeScheduler(LoadScopeScheduling):
def _split_scope(self, nodeid):
return nodeid.split("/", 1)[0] def pytest_xdist_make_scheduler(config, log):
return CustomizeScheduler(config, log)
  1. 只需在最外层conftest中继承xdist.scheduler.LoadScopeScheduling并重写_split_scope方法
  2. 重写钩子函数pytest_xdist_make_scheduler
pytest -v -n 4 --dist=loadfile

第五步:worker执行测试用例

  1. workers 重写了pytest_runtestloop:pytest的默认实现是循环执行所有在test_session这个对象里面收集到的测试用例。
  2. 但是在xdist里, workers实际上是等待master为其发送需要执行的测试用例。
  3. 当worker收到测试任务, 就顺序执行pytest_runtest_protocol
  4. 值得注意的一个细节是:workers 必须始终保持至少一个测试用例在的任务队列里, 以兼容pytest_runtest_protocol(item, nextitem)hook的参数要求,为了将nextitem传给hook。
  5. master在worker执行完分配的一组测试后,基于测试执行时长以及每个worker剩余测试用例综合决定是否向这个worker发送更多的测试用例。
  6. worker会在执行最后一个测试项前等待master的更多指令。
  7. 如果它收到了更多测试项, 那么就可以安全的执行 pytest_runtest_protocol,因为这时nextitem参数已经可以确定。
  8. 如果它收到一个 shutdown信号, 那么就将nextitem参数设为None, 然后执行 pytest_runtest_protocol

第六步:测试结束

  1. 当master没有更多执行测试任务时,它会发送一个shutdown信号给所有worker。
  2. 当worker将剩余测试用例执行完后退出进程。
  3. 当workers在测试执行结束时,会将结果被发送回master,然后master将结果转发到其他pytest hooks比如:pytest_runtest_logstartpytest_runtest_logreport 确保整个测试活动进行正常运作。
  4. master等待所有worker全部退出并关闭测试会话。

注意:pytest-xdist 是让每个 worker 进程执行属于自己的测试用例集下的所有测试用例。这意味着在不同进程中,不同的测试用例可能会调用同一个 scope 范围级别较高(例如session)的 fixture,该 fixture 则会被执行多次,这不符合 scope=session 的预期。

pytest-xdist 没有内置的支持来确保会话范围的 fixture 仅执行一次,但是可以通过使用锁定文件进行进程间通信来实现;让scope=session 的 fixture 在 test session 中仅执行一次。

示例:需要安装 filelock 包,安装命令pip install filelock

  1. 比如只需要执行一次login(或定义配置选项、初始化数据库连接等)。
  2. 当第一次请求这个fixture时,则会利用FileLock仅产生一次fixture数据。
  3. 当其他进程再次请求这个fixture时,则不会重复执行fixture。
import pytest
from filelock import FileLock @pytest.fixture(scope="session")
def login(tmp_path_factory, worker_id):
# 代表是单机运行
if worker_id == "master":
token = str(random())
print("fixture:请求登录接口,获取token", token)
os.environ['token'] = token return token # 分布式运行
# 获取所有子节点共享的临时目录,无需修改【不可删除、修改】
root_tmp_dir = tmp_path_factory.getbasetemp().parent
fn = root_tmp_dir / "data.json"
with FileLock(str(fn) + ".lock"):
if fn.is_file(): # 代表已经有进程执行过该fixture
token = json.loads(fn.read_text())
else: # 代表该fixture第一次被执行
token = str(random())
fn.write_text(json.dumps(token))
# 最好将后续需要保留的数据存在某个地方,比如这里是os的环境变量
os.environ['token'] = token
return token

多线程执行用例之 pytest-parallel

用于并行并发测试的 pytest 插件

pip install pytest-parallel

常用参数配置

  1. --workers=n :多进程运行需要加此参数, n是进程数。默认为1

  2. --tests-per-worker=n :多线程需要添加此参数,n是线程数

如果两个参数都配置了,就是进程并行;每个进程最多n个线程,总线程数:进程数*线程数

【注意】

  1. 在windows上进程数永远为1。

  2. 需要使用 if name == “main” :在命令行窗口运行测试用例会报错

示例:

  • pytest test.py --workers 3 :3个进程运行
  • pytest test.py --tests-per-worker 4 :4个线程运行
  • pytest test.py --workers 2 --tests-per-worker 4 :2个进程并行,且每个进程最多4个线程运行,即总共最多8个线程运行。
    import pytest
    
    def test_01():
    print('测试用例1操作') def test_02():
    print('测试用例2操作') def test_03():
    print('测试用例3操作') def test_04():
    print('测试用例4操作') def test_05():
    print('测试用例5操作') def test_06():
    print('测试用例6操作') def test_07():
    print('测试用例7操作') def test_08():
    print('测试用例8操作') if __name__ == "__main__":
    pytest.main(["-s", "test_b.py", '--workers=2', '--tests-per-worker=4'])

pytest-parallel与pytest-xdist对比说明:

  • pytest-parallel 比 pytst-xdist 相对好用,功能支持多;
  • pytst-xdist 不支持多线程;
  • pytest-parallel 支持python3.6及以上版本,所以如果想做多进程并发在linux或者mac上做,在Windows上不起作用(Workers=1),如果做多线程linux/mac/windows平台都支持,进程数为workers的值。
  • pytest-xdist适用场景为:
    • 不是线程安全的
    • 多线程时性能不佳的测试
    • 需要状态隔离
  • pytest-parallel对于某些用例(如 Selenium)更好:
    • 可以是线程安全的
    • 可以对 http 请求使用非阻塞 IO 来提高性能

简而言之,pytest-xdist并行性pytest-parallel是并行性和并发性。

pytest多进程/多线程执行测试用例的更多相关文章

  1. Selenium_多线程执行测试用例

    多线程执行测试用例 这里以百度搜索为例,通过不同的浏览器来启动不同的线程. #!/usr/bin/env python # _*_ coding:utf-8 _*_ __author__ = 'Yin ...

  2. Python单元测试框架之pytest 1 ---如何执行测试用例

    From: https://www.cnblogs.com/fnng/p/4765112.html 介绍   pytest是一个成熟的全功能的Python测试工具,可以帮助你写出更好的程序. 适合从简 ...

  3. pytest(13)-多线程、多进程执行用例

    有些项目的测试用例较多,测试用例时需要分布式执行,缩短运行时间. pytest框架中提供可用于分布式执行测试用例的插件:pytest-parallel.pytest-xdist,接下来我们来学习这两个 ...

  4. pytest测试框架 -- skip跳过执行测试用例

      跳过执行测试用例 1.@pytest.mark.skip(reason=" ") -- 跳过执行测试函数 可传入一个非必须参数reason表示原因 import pytest@ ...

  5. Pytest(16)随机执行测试用例pytest-random-order

    前言 通常我们认为每个测试用例都是相互独立的,因此需要保证测试结果不依赖于测试顺序,以不同的顺序运行测试用例,可以得到相同的结果. pytest默认运行用例的顺序是按模块和用例命名的 ASCII 编码 ...

  6. 『德不孤』Pytest框架 — 6、Mark分组执行测试用例

    目录 1.Pytest中的Mark介绍 2.Mark的使用 3.Mark的注册和使用 4.使用Mark完成失败重试 5.扩展 1.Pytest中的Mark介绍 Mark主要用于在测试用例/测试类中给用 ...

  7. Python单元测试框架之pytest---如何执行测试用例

    介绍   pytest是一个成熟的全功能的Python测试工具,可以帮助你写出更好的程序. 适合从简单的单元到复杂的功能测试 l 模块化parametrizeable装置(在2.3,持续改进) l 参 ...

  8. pthread_create多线程执行顺序诡异现象

    多线程执行顺序诡异现象谈,你不知道的pthread_create 引文:学而时习之,不亦说乎.总是忙于具体项目,业务功能的实现:关于编程本身的技能都要有些生疏了,于是就选择了几个专题做了一次温习,重点 ...

  9. gdb常用命令及使用gdb调试多进程多线程程序

    一.常用普通调试命令 1.简单介绍GDB 介绍: gdb是Linux环境下的代码调试⼯具.使⽤:需要在源代码⽣成的时候加上 -g 选项.开始使⽤: gdb binFile退出: ctrl + d 或 ...

随机推荐

  1. MATLAB地图工具箱学习心得(二)设计可变参数和位置拾取的“放大镜”式投影程序

    最近刚好因为一些原因整理这方面的内容,所以还是把这篇鸽了一年多的博客顺手写出来了∠( ᐛ 」∠)_.因为是当时课程设计的一部分,程序上难免会有一些不足和bug,在这里将设计的思路分享给大家. 本篇博客 ...

  2. .NET Core企业微信网页授权登录

    1.开发前准备 参数获取 corpid 每个企业都拥有唯一的corpid,获取此信息可在管理后台"我的企业"-"企业信息"下查看"企业ID" ...

  3. java 中为什么重写 equals 后需要重写 hashCode

    本文为博主原创,未经允许不得转载: 1. equals 和 hashCode 方法之间的关系 这两个方法都是 Object 的方法,意味着 若一个对象在没有重写 这两个方法时,都会默认采用 Objec ...

  4. day01-从一个基础的socket服务说起

    首发地址:day01-从一个基础的socket服务说起 教程说明:C++高性能网络服务保姆级教程 本节目的 实现一个基于socket的echo服务端和客户端 服务端监听流程 第一步:使用socket函 ...

  5. Java 18为什么要指定UTF-8为默认字符集

    在Java 18中,将UTF-8指定为标准Java API的默认字符集.有了这一更改,依赖于默认字符集的API将在所有实现.操作系统.区域设置和配置中保持一致. 做这一更改的主要目标: 当Java程序 ...

  6. HCNP Routing&Switching之Super VLAN

    前文我们了解了VLAN隔离技术MUX VLAN相关话题,回顾请参考https://www.cnblogs.com/qiuhom-1874/p/16196936.html:今天我们来聊一聊VLAN优化S ...

  7. 借助ADB冻结与卸载Android系统应用(免ROOT)

    背景: 我妈的手机饱受系统应用广告推送之苦,每天都能在通知栏里收到好几条广告.为了给她个清净,本篇博文应运而生. 目标: 卸载安卓系统应用 所用工具: 硬件:我妈的手机(魅蓝5) PC端:Minima ...

  8. Java编程小技巧(1)——方法传回两个对象

    原文地址:Java编程小技巧(1)--方法传回两个对象 | Stars-One的杂货小窝 题目是个伪命题,由Java语法我们都知道,方法要么返回一个对象,要么就不返回 当有这样的情况,我们需要返回两个 ...

  9. v82.01 鸿蒙内核源码分析 (协处理器篇) | CPU 的好帮手 | 百篇博客分析 OpenHarmony 源码

    本篇关键词:CP15 .MCR.MRC.ASID.MMU 硬件架构相关篇为: v65.01 鸿蒙内核源码分析(芯片模式) | 回顾芯片行业各位大佬 v66.03 鸿蒙内核源码分析(ARM架构) | A ...

  10. maven install resources failed: newPosition < 0: (-1 < 0)

    添加以下代码在 pom.xml 中,具体参阅这里 <build> <plugins> <plugin> <groupId>org.apache.mave ...