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

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

 

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

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

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

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

 

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

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

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

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

 

1、安装

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



pip install pytest-xdist


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



pip install pytest-xdist -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com


 

2、示例

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

如图所示:项目目录结构

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

根目录下conftest.py文件

脚本代码:



#!/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文件

脚本代码:



#!/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文件

脚本代码:



#!/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文件

脚本代码:



#!/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文件

脚本代码:



#!/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文件

脚本代码:



#!/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文件

脚本代码:



#!/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、不使用分布式测试执行测试用例

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



pytest -s


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

Python测试框架pytest(22)插件 - pytest-xdist(分布式执行)_根目录_02

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

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

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

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



pytest -s -n auto


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

Python测试框架pytest(22)插件 - pytest-xdist(分布式执行)_根目录_03

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

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



pytest -s -n 5


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

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

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

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



pytest -s -n auto --html=report.html --self-contained-html


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

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

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

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时,则会从文件中读取数据。

脚本代码:



#!/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测试报告

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

根目录下conftest.py文件

脚本代码:



#!/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文件

脚本代码:



#!/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文件

脚本代码:



#!/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文件

脚本代码:



#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
微信公众号:AllTests软件测试
"""

import os

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


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



pytest -n 3 --alluredir=./allure
allure serve allure


3、运行结果:

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

Python测试框架pytest(22)插件 - pytest-xdist(分布式执行)_根目录_08 

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

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

 

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

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

脚本代码:



#!/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、打开命令行,在该项目根目录下,输入执行命令



pytest -n 3 --alluredir=./allure
allure serve allure


3、运行结果:

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

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

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

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

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

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

 

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

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

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



pytest -n 2 --alluredir=./allure
allure serve allure


2、运行结果:

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

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

Python测试框架pytest(22)插件 - pytest-xdist(分布式执行)_根目录_15

Python测试框架pytest(22)插件 - pytest-xdist(分布式执行)_根目录_16