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的测试用例就没有读缓存文件,每个进程只会读一次缓存文件。