使用pytest-xdist实现分布式APP自动化测试:基于SSH

1|0前言


pytest-xdist是一款分布式测试插件,它有两种方式实现master和worker的远程通讯,一种是SSH,另一种是socket。本文将介绍如何使用SSH实现用例同步、用例执行以及报告收集

2|0项目环境


2|1系统环境


角色 系统 Python版本 ip
master Centos7.6 v3.8.0 192.168.0.109
worker1 Centos7.6 v3.8.0 192.168.0.126
worker2 Centos7.6 v3.8.0 192.168.0.136

2|2客户端环境


测试机 系统 ip tcpip
逍遥模拟器-1 v7.1.2 192.168.0.112 6666
逍遥模拟器-2 v7.1.2 192.168.0.113 6666

2|3运行环境


容器 版本 端口号 宿主机器
appium_2 v1.17.0 4725->4723 worker1
appium_2 v1.17.0 4725->4723 worker2

3|0项目结构


主要的测试用例存储在test_case.py中,main.py是运行入口

APP_Xdist_AutoTest |--allure_reports |--common |--base_driver.py ... |--caps |--cpas.py |--images |--html_reports |--logs |--page_objects |--base_page.py |--login_page.py ... |--test_cases |--conftest.py |--test_login.py ... |--test_datas |--login_data.py |--pytest.ini |--main.py |--README.md |--requirements.txt

4|0准备工作


4|1挂载报告目录


因为pytest-xdist对pytest-html支持较好,对allure_pytest支持的不太好。使用allure运行结束后会出现一个问题,即生成html报告要用到的json和txt数据都分散在各个worker的报告目录下,master上报告目录下是空的。因此需要将各个worker的报告目录挂载到master的报告目录,运行过程中,对worker的报告目录写入操作,实际上就是写入master
挂载目录前需要安装一个sshfs,如何安装sshfs请戳这里-->《使用sshfs挂载远程文件系统》

4|2临时挂载和开机自动挂载


临时挂载,重启后挂载的信息就没了,临时挂载命令是:

sshfs root@192.168.0.109:/var/lib/jenkins/workspace/APP_Xdist_AutoTest/allure_reports /opt/pyexecnetcache/APP_Xdist_AutoTest/allure_reports

前面的路径是你要挂载到的master上的报告目录,后面的路径是worker上的报告目录,这个命令就是把worker的报告目录挂载到master上
注意两个问题:
1.挂载操作是在worker上执行
2.必须保证/opt/pyexecnetcache/APP_Xdist_AutoTest/allure_reports这个路径中的allure_reports是空目录,否则挂载会报错
3.临时挂载中会要求输入密码,怎么设置公钥实现无密码挂载,请参考上面那篇文章

开机自动挂载,又称为永久挂载,就是开机后会自动执行shell脚本,shell脚本中是挂载命令,所以开机看到的就是已挂载好的情况。怎么设置开机自动挂载,主要有以下几步:
1.在/opt目录下,新建一个mount.sh的文件,往里面写入挂载命令

# mount.sh sshfs root@192.168.0.109:/var/lib/jenkins/workspace/APP_Xdist_AutoTest/allure_reports /opt/pyexecnetcache/APP_Xdist_AutoTest/allure_reports

2.给mount.sh赋予可执行权限

chmod +x mount.sh

3.将sh mount.sh写入/etc/rc.local中,/etc/rc.local是/etc/rc.d/rc.local的软连接,后者是用于添加开机自启动命令

echo sh /opt/mount.sh >> /etc/rc.local

4.由于centos7中/etc/rc.d/rc.local降级了,不具备可执行权限,因为要给/etc/rc.local赋予可执行权限

chmod 777 /etc/rc.local

4|3模拟器设置


1.启动两个系统版本一致(v7.1.2)的逍遥模拟器
2.设置桥接,设置方法请戳这里-->《docker创建appium容器并连接夜神模拟器》
3.设置tcpip端口号

C:\Users\beck>adb devices List of devices attached 192.168.0.112:62001 device 192.168.0.113:62026 device C:\Users\beck>adb -s 192.168.0.112:62001 tcpip 6666 restarting in TCP mode port: 6666 C:\Users\beck>adb -s 192.168.0.113:62026 tcpip 6666 restarting in TCP mode port: 6666

4|4启动服务


这里的步骤是两个worker都要执行的,只不过通过tcpip连接模拟器的时候,一个worker连接一个,当看到类似"connected to 192.168.0.112:6666"的提示出现时,说明已经连接成功了

# 关闭/禁用防火墙 systemctl stop firewalld systemctl disable firewalld # 启动docker容器 systemctl start docker # 启动appium_2容器 docker start appium_2 # 通过tcpip连接模拟器 # worker1 docker exec -it appium_2 adb connect 192.168.0.112:6666 # worker2 docker exec -it appium_2 adb connect 192.168.0.113:6666

5|0同步运行


5|1同步方式


在开始同步前,在master上生成rsa公钥,然后将公钥分别拷贝到两个worker上,这样ssh同步测试用例时,才会禁用输入密码的操作。同样,怎么配置请戳这里-->《使用sshfs挂载远程文件系统》
官网中给出的操作很简单,就一句命令

pytest -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg

这是pytest-xdist值得吐槽的地方,插件是好插件,文档写的太简略了。一开始我也不知道ssh怎么实现同步,直到反复看了issue和其他文档后,才发现也不难
官方提到的一个execnet的网站很重要,在这个网站上,你可以看到pytest-xdist实现分发的重要理论依据,即它是用python自带的库execnet,内部实例化了一个网关,通过网关实现代码管理和数据通信。实现(实例化)官网有很多方法,我们要用到ssh和socket都包含在里面

主要看这句话,它说ssh=wyvern//python=python3.3//chdir=mycache,在主机上wyvern上指定了一个python3.3的解释器,后面的chdir表示远程的进程会将mycache作为它当前的工作目录

ssh=wyvern//python=python3.3//chdir=mycache specifies a Python3.3 interpreter on the host wyvern. The remote process will have mycache as its current working directory.

在理解这句话的基础上,经过我反复的试验,发现ssh同步有两种方式:
1.指定同步目录
以我的环境为例,我要把当前目录下的TestDemo目录同步到远程的/opt/pyexecnetcache目录下,只需要运行下面的命令。这个命令在同步的时候,也运行了测试用例

pytest -d --tx 'ssh=root@192.168.0.126//python=/opt/Python-3.8.0/bin/python3.8//chdir=/opt/pyexecnetcache' --tx 'ssh=root@192.168.0.136//python=/opt/Python-3.8.0/bin/python3.8//chdir=/opt/pyexecnetcache' --rsyncdir ./Test_Demo

master

worker1

worker2

如果不想要外面的TestDemo,只想要test_demo.py,需要将同步目录改成--rsyncdir ./Test_Demo/*

2.不指定同步目录
不指定同步目录,同步过去的目录将是root的家目录即/root,不管你当前的所在位置是在哪

pytest -d --tx 'ssh=root@192.168.0.126//python=/opt/Python-3.8.0/bin/python3.8' --tx 'ssh=root@192.168.0.136//python=/opt/Python-3.8.0/bin/python3.8' --rsyncdir ./Test_Demo/

一开始worker1在根目录,worker2在/root目录,得到的结果是两个都在/root目录生成了pyexecnetcache目录,pyexecnetcache目录下是TestDemo
master

worker1

worker2

看到这里或许有点疑问,不会官网写的是pytest -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg吗?怎么少了最后一个mypkg?最后一个mypkg不是指定同步目录的吗?经过我多次试验,最后一个mypkg没有存在的必要,你将它命名为test,它也不会再worker上创建一个test出来,来存放pyexecnetcache。同步的目录创建的顶层目录永远是pyexecnetcache

5|2同步运行


之所以要同步项目,而不是同步测试用例,因为真实的APP项目的测试用例依赖driver的生成、测试数据、公共方法等一系列的其他包模块,如果只是将test_cases包发过去,运行肯定都是失败的。因此要做整个项目的同步
在这里建议将同步目录制定为/opt/pyexecnetcache,因为挂载点就是/opt/pyexecnetcache/APP_Xdist_AutoTest/allure_reports,事实上挂载决定了测试报告目录只能是固定的路径

pytest -d --tx 'ssh=root@192.168.0.126//python=/opt/Python-3.8.0/bin/python3.8//chdir=/opt/pyexecnetcache' --tx 'ssh=root@192.168.0.136//python=/opt/Python-3.8.0/bin/python3.8//chdir=/opt/pyexecnetcache' --rsyncdir ./APP_Xdist_AutoTest

master

worker1

worker2

可以看到,同步成功了,运行却失败了。这是什么原因呢?和上面说的一样,我们的项目依赖非常复杂,pytest -d --tx...运行的只是相当于运行pytest test_case/test_xxxx.py,这时估计driver都没生成,所以运行失败了
因此要用python3 main.py的方式来运行,因为driver传命令行参数给conftest.py,conftest.py里的fixture再调用base_driver()生成driver返回给测试用例,测试用例将fixture拿到,作为参数传给各个页面对象,以此来驱动页面对象获取元素,执行相应的操作
那么,问题来了,之前一直是pytest命令行,现在写在main.py里该怎么写?其实pytest-xdist有一个优点就是保持了一部分pytest的使用习惯,pytest命令行怎么执行冒烟呢?pytest -m smoke,放在main.py中怎么写: pytest.main([ "-m", "smoke"])。那么,照猫画虎就行

# main.py import pytest import time from multiprocessing import Pool from common.clean import * device_infos = [{"docker_name": "appium_2", "platform_version": "7.1.2", "server_port": 4725}] cur_time = time.strftime("%Y-%m-%d_%H-%M-%S") def run_parallel(device_info): pytest.main([ "-d", "--tx", "ssh=root@192.168.0.126//python=/opt/Python-3.8.0/bin/python3.8//chdir=/opt/pyexecnetcache", "--tx", "ssh=root@192.168.0.136//python=/opt/Python-3.8.0/bin/python3.8//chdir=/opt/pyexecnetcache", "--rsyncdir", "./", "APP_Xdist_AutoTest", #因为worker的位置是/opt/pyexecnetcache,下面还有一个APP_Xdist_AutoTest目录,最里面才是test_cases,因此需要指定APP_Xdist_AutoTest,表示进入到APP_Xdist_AutoTest里执行pytest f"--cmdopt={device_info}", #"--junitxml", f"{html_reports_dir}/autotest_report_{cur_time}.xml", #"--html", f"{html_reports_dir}/autotest_report_{cur_time}.html", #"--css", f"{html_reports_dir}/assets/style.css", #"--self-contained-html", "--alluredir", allure_reports_dir ]) os.system(f"allure generate {allure_reports_dir} -o {allure_reports_dir}/html --clean") if __name__ == "__main__": with Pool(1) as pool: pool.map(run_parallel, device_infos) pool.close() pool.join()

然后单独运行python3 main.py就能成功。13个用例耗时3min35s,之前用单个机器运行是6min49s,可以看到三台分布式运行APP自动化应该能节省近一半的时间

6|0报告预览


服务器上导出的报告容易出现样式缺失,因此要将项目放在jenkins上运行,得到在线报告

7|0参考文章


《pytest-xdist官网》
《execnet官网》
《使用SSHFS挂载远程VPS目录并设置开机启动》


__EOF__

本文作者cnhkzyy
本文链接https://www.cnblogs.com/my_captain/p/12789144.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   cnhkzyy  阅读(1804)  评论(2编辑  收藏  举报
编辑推荐:
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
点击右上角即可分享
微信分享提示