Python测试框架pytest(22)插件 - pytest-xdist(分布式执行)

2022-07-26 14:33:48 浏览数 (1)

Python测试框架pytest(22)

插件

pytest-xdist(分布式执行)

目录

  • 1、安装
  • 2、示例
  • 3、原理和流程
  • 4、解决:多进程运行次数

当测试用例非常多的时候,一条条按顺序执行测试用例,是很浪费测试时间的。这时候就可以用到 pytest-xdist,让自动化测试用例可以分布式执行,从而大大节省测试时间。

pytest-xdist 是属于进程级别的并发。

分布式测试用例的设计原则:

(1)独立运行:用例之间是独立的,并且没有依赖关系,还可以完全独立运行。

(2)随机执行:用例执行不强制按顺序执行,支持顺序执行或随机执行。

(3)不影响其他用例:每个用例都能重复运行,运行结果不会影响其他用例。

pytest-xdist 通过一些独特的测试执行模式扩展了 pytest:

(1)测试运行并行化:如果有多个CPU或主机,则可以将它们用于组合的测试运行。这样可以加快开发速度或使用远程计算机的特殊资源。

(2)--looponfail:在子进程中重复运行测试。每次运行之后,pytest 都会等到项目中的文件更改后再运行之前失败的测试。重复此过程,直到所有测试通过,然后再次执行完整运行。

(3)跨平台覆盖:可以指定不同的 Python 解释器或不同的平台,并在所有这些平台上并行运行测试。

1、安装

在命令行中运行以下命令进行安装:

代码语言:javascript复制
pip install pytest-xdist

或者(使用国内的豆瓣源,数据会定期同步国外官网,速度快。)

代码语言:javascript复制
pip install pytest-xdist -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com

2、示例

创建My_pytest_Demo3项目,并创建如下文件。

如图所示:项目目录结构

根目录下conftest.py文件

脚本代码:

代码语言:javascript复制
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
微信公众号:AllTests软件测试
"""

import pytest

@pytest.fixture(scope="session")
def login():
    print("===登录,返回:name,token===")
    name = "AllTests"
    token = "123456qwe"
    yield name, token
    print("===退出===")

根目录下test_case.py文件

脚本代码:

代码语言:javascript复制
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
微信公众号:AllTests软件测试
"""

import pytest
from time import sleep

@pytest.mark.parametrize("n", list(range(5)))
def test_get_info(login, n):
    sleep(1)
    name, token = login
    print("===获取用户个人信息===", n)
    print(f"用户名:{name}, token:{token}")

test_baidu包下conftest.py文件

脚本代码:

代码语言:javascript复制
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
微信公众号:AllTests软件测试
"""

import pytest

@pytest.fixture(scope="module")
def open_baidu(login):
    name, token = login
    print(f"===用户 {name} 打开baidu===")

test_baidu包下test_case1.py文件

脚本代码:

代码语言:javascript复制
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
微信公众号:AllTests软件测试
"""

import pytest
from time import sleep

@pytest.mark.parametrize("n", list(range(5)))
def test_case1_1(open_baidu, n):
    sleep(1)
    print("===baidu 执行测试用例test_case1_1===", n)

@pytest.mark.parametrize("n", list(range(5)))
def test_case1_2(open_baidu, n):
    sleep(1)
    print("===baidu 执行测试用例test_case1_2===", n)

test_weibo包下test_case2.py文件

脚本代码:

代码语言:javascript复制
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
微信公众号:AllTests软件测试
"""

import pytest
from time import sleep

@pytest.mark.parametrize("n", list(range(5)))
def test_case2_no_fixture(login, n):
    sleep(1)
    print("===weibo 没有__init__测试用例,执行测试用例test_case2_no_fixture===", login)

test_douyin包下conftest.py文件

脚本代码:

代码语言:javascript复制
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
微信公众号:AllTests软件测试
"""

import pytest

@pytest.fixture(scope="function")
def open_douyin(login):
    name, token = login
    print(f"===用户 {name} 打开douyin===")

test_douyin包下test_case3.py文件

脚本代码:

代码语言:javascript复制
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
微信公众号:AllTests软件测试
"""

import pytest
from time import sleep

@pytest.mark.parametrize("n", list(range(5)))
class TestDouyin:
    def test_case3_1(self, open_douyin, n):
        sleep(1)
        print("===douyin 执行测试用例test_case3_1===", n)

    def test_case3_2(self, open_douyin, n):
        sleep(1)
        print("===douyin 执行测试用例test_case3_2===", n)

1、不使用分布式测试执行测试用例

打开命令行,在该项目根目录下,输入执行命令

代码语言:javascript复制
pytest -s

执行一条用例大概1s,因为每个用例都加了sleep(1),一共30条用例,总共运行30.16s。

2、使用分布式测试执行测试用例

参数 -n auto:可以自动检测到系统的CPU核数。

使用 auto 等于利用了所有CPU来跑用例,此时CPU占用率会特别高。

打开命令行,在该项目根目录下,输入执行命令

代码语言:javascript复制
pytest -s -n auto

执行30条用例,只用了4.81s。

3、使用分布式测试执行测试用例(指定多少进程)

打开命令行,在该项目根目录下,输入执行命令

代码语言:javascript复制
pytest -s -n 5

指定5个进程同时执行30条用例,用时6.99s。

4、pytest-xdist 和 pytest-html 联合使用

打开命令行,在该项目根目录下,输入执行命令

代码语言:javascript复制
pytest -s -n auto --html=report.html --self-contained-html

执行完成后自动生成的报告

5、按照一定顺序执行

pytest-xdist 默认是无序执行的,可以通过 --dist 参数来控制执行顺序。

--dist=loadscope:将按照同一个模块 module 下的函数和同一个测试类 class 下的方法来分组,然后将每个测试组发给可以执行的 worker,确保同一个组的测试用例在同一个进程中执行。目前无法自定义分组,按类 class 分组优先于按模块 module 分组。

--dist=loadfile:按照同一个文件名来分组,然后将每个测试组发给可以执行的 worker,确保同一个组的测试用例在同一个进程中执行。

6、使 scope=session 的 fixture 在 test session 中仅执行一次

pytest-xdist 是让每个 worker 进程执行属于自己的测试用例集下的所有测试用例。

这意味着在不同进程中,不同的测试用例可能会调用同一个 scope 范围级别较高(例如session)的 fixture,该 fixture 则会被执行多次,这不符合 scope=session 的预期。

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

示例:

(1)该示例只需要执行一次login(如只需要执行一次来定义配置选项等)。

(2)当第一次请求这个fixture时,则会利用FileLock仅产生一次fixture数据。需要安装filelock包,安装命令pip install filelock

(3)当其他进程再次请求这个fixture时,则会从文件中读取数据。

脚本代码:

代码语言:javascript复制
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
微信公众号:AllTests软件测试
"""

import pytest
from filelock import FileLock

@pytest.fixture(scope="session")
def login():
    print("===登录,返回:name,token===")
    with FileLock("session.lock"):
        name = "AllTests"
        token = "123456qwe"

        # Web App UI自动化,声明一个driver,再返回
        # 接口自动化,发起一个登录请求,将token返回

    yield name, token
    print("===退出===")

3、原理和流程

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

在 xdist 中,主是 master,从是 workers。

分布式测试的原理:

(1)xdist 会产生一个或多个 workers,workers 都通过 master 来控制;

(2)每个 worker 负责执行完整的测试用例集,然后按照 master 的要求运行测试,而 master 机不执行测试任务。

分布式测试的流程:

1、创建 worker

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

(2)master 和 worker 之间是通过 execnet 和网关来通信的;

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

2、收集测试用例

(1)每个 worker 类似一个迷你型的 pytest 执行器;

(2)worker 会执行一个完整的 test collection 过程(收集所有测试用例的过程);

(3)然后把测试用例的 ids 返回给 master;

(4)master 是不会执行任何测试用例集的。

注:所以为什么脚本代码里有打印语句(print)通过分布式测试时结果没有输出用例的打印内容,因为主机并不执行测试用例,PyCharm 相当于一个 master。

3、master 检测 workers 收集到的测试用例集

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

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

(3)所有的节点都保存着相同的测试用例集,并且使用这种方式可以节省带宽,因为 master 只需要告知 workers 需要执行的测试用例对应的索引,而不用告知完整的测试用例信息。

4、测试用例分发

--dist-mode 选项

each:master 将完整的测试索引列表分发到每个 worker。

load:master 将大约25%的测试用例以轮询的方式分发到各个 worker,剩余的测试用例则会等待 workers 执行完测试用例以后再分发。

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

5、测试用例的执行

(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)worker 会在执行最后一个测试项前等待 master 的更多指令;

(6)如果它收到了更多测试项, 那么就可以安全的执行 pytest_runtest_protocol,因为这时 nextitem 参数已经可以确定;

(7)如果它收到一个 "shutdown" 信号, 那么就将 nextitem 参数设为 None, 然后执行 pytest_runtest_protocol。

6、测试用例再分发

--dist-mode=load

(1)当 workers 开始/结束执行时,会把测试结果返回给 master,这样其他 pytest hook 比如(pytest_runtest_protocol 和 pytest_runtest_protocol 就可以正常执行);

(2)master 在 worker 执行完一个测试后,基于测试执行时长以及每个 work 剩余测试用例综合决定是否向这个 worker 发送更多的测试用例。

7、测试结束

(1)当 master 没有更多执行测试任务时,它会发送一个 "shutdown" 信号给所有 worker;

(2)当 worker 将剩余测试用例执行完后退出进程;

(3)master 等待所有 worker 全部退出;

(4)此时仍需要处理诸如 pytest_runtest_logreport 等事件。

4、解决:多进程运行次数

如何保证 scope=session 的 fixture 在多进程运行情况下仍然只运行一次。

1、创建My_pytest_Demo3_2项目,并创建如下文件。

如图所示:项目目录结构,allure文件夹存放allure测试报告

根目录下conftest.py文件

脚本代码:

代码语言:javascript复制
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
微信公众号:AllTests软件测试
"""

import os
import pytest
from random import random

@pytest.fixture(scope="session")
def test():
    token = str(random())
    print("fixture:请求登录接口,获取token", token)
    os.environ['token'] = token
    return token

根目录下test_case1.py文件

脚本代码:

代码语言:javascript复制
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
微信公众号:AllTests软件测试
"""

import os

def test_one(test):
    print("os 环境变量:", os.environ['token'])
    print("test_one 测试用例", test)

根目录下test_case2.py文件

脚本代码:

代码语言:javascript复制
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
微信公众号:AllTests软件测试
"""

import os

def test_two(test):
    print("os 环境变量:", os.environ['token'])
    print("test_two 测试用例", test)

根目录下test_case3.py文件

脚本代码:

代码语言:javascript复制
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
微信公众号:AllTests软件测试
"""

import os

def test_three(test):
    print("os 环境变量:", os.environ['token'])
    print("test_three 测试用例", test)

2、打开命令行,在该项目根目录下,输入执行命令

代码语言:javascript复制
pytest -n 3 --alluredir=./allure
allure serve allure

3、运行结果:

scope=session的fixture执行了三次,三个进程下的三个测试用例得到的数据不一样。

一、解决 scope=session 的 fixture 在多进程运行情况下仍然只运行一次

1、修改根目录下conftest.py文件

脚本代码:

代码语言:javascript复制
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
微信公众号:AllTests软件测试
"""
import json
import os
import pytest
from random import random
from filelock import FileLock

@pytest.fixture(scope="session")
def test(tmp_path_factory, worker_id):
    # 如果是单机运行,则运行这里的代码块【不可删除、修改】
    if worker_id == "master":
        """
        【自定义代码块】
        这里就写你要本身要做的操作,比如:登录请求、新增数据、清空数据库历史数据等
        """
        token = str(random())
        print("fixture:请求登录接口,获取token", token)
        os.environ['token'] = 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():
            # 缓存文件中读取数据,像登录操作的话就是token【不可删除、修改】
            token = json.loads(fn.read_text())
            print(f"读取缓存文件,token是:{token}")
        else:
            """
            【自定义代码块】
            跟上面if的代码块一样就行
            """
            token = str(random())
            print("fixture:请求登录接口,获取token", token)
            # 【不可删除、修改】
            fn.write_text(json.dumps(token))
            print(f"首次执行,token是:{token}")

        # 最好将后续需要保留的数据存在某个地方,比如这里是os的环境变量
        os.environ['token'] = token
    return token

2、打开命令行,在该项目根目录下,输入执行命令

代码语言:javascript复制
pytest -n 3 --alluredir=./allure
allure serve allure

3、运行结果:

可以看到fixture只执行了一次,不同进程下的测试用例共享一个数据token。

(1)读取缓存文件并不是每个测试用例都会读,它是按照进程来读取的,比如 -n 3 指定三个进程运行,那么有一个进程会执行一次 fixture(随机),另外两个进程会各读一次缓存。

(2)假设每个进程有很多个用例,那也只是读一次缓存文件,而不会读多次缓存文件。所以最好将从缓存文件读出来的数据保存在指定的地方,比如 os.environ 将数据保存在环境变量中。

二、进程少测试用例多的情况下执行

例如:两个进程跑三个测试用例

1、打开命令行,在该项目根目录下,输入执行命令

代码语言:javascript复制
pytest -n 2 --alluredir=./allure
allure serve allure

2、运行结果:

可以看到test_three的测试用例就没有读缓存文件,每个进程只会读一次缓存文件。

0 人点赞