前言
pytest-xdist是一款分布式測試插件,它有兩種方式實現master和worker的遠程通訊,一種是SSH,另一種是socket。本文將介紹如何使用SSH實現用例同步、用例執行以及報告收集
項目環境
系統環境
| 角色 | 系統 | 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 |
客戶端環境
| 測試機 | 系統 | ip | tcpip |
|---|---|---|---|
| 逍遙模擬器-1 | v7.1.2 | 192.168.0.112 | 6666 |
| 逍遙模擬器-2 | v7.1.2 | 192.168.0.113 | 6666 |
運行環境
| 容器 | 版本 | 端口號 | 宿主機器 |
|---|---|---|---|
| appium_2 | v1.17.0 | 4725->4723 | worker1 |
| appium_2 | v1.17.0 | 4725->4723 | worker2 |
項目結構
主要的測試用例存儲在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
准備工作
掛載報告目錄
因為pytest-xdist對pytest-html支持較好,對allure_pytest支持的不太好。使用allure運行結束后會出現一個問題,即生成html報告要用到的json和txt數據都分散在各個worker的報告目錄下,master上報告目錄下是空的。因此需要將各個worker的報告目錄掛載到master的報告目錄,運行過程中,對worker的報告目錄寫入操作,實際上就是寫入master
掛載目錄前需要安裝一個sshfs,如何安裝sshfs請戳這里-->《使用sshfs掛載遠程文件系統》
臨時掛載和開機自動掛載
臨時掛載,重啟后掛載的信息就沒了,臨時掛載命令是:
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
模擬器設置
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
啟動服務
這里的步驟是兩個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
同步運行
同步方式
在開始同步前,在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
同步運行
之所以要同步項目,而不是同步測試用例,因為真實的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自動化應該能節省近一半的時間

報告預覽
服務器上導出的報告容易出現樣式缺失,因此要將項目放在jenkins上運行,得到在線報告

