首頁 > 軟體

pytest多執行緒與多裝置並行appium

2022-06-30 14:02:17

1、appium+python 實現單裝置的 app 自動化測試

  • 啟動 appium server,佔用埠 4723
  • 電腦與一個裝置連線,通過 adb devices 獲取已連線的裝置
  • 在 python 程式碼當中,編寫啟動引數,通過 pytest 編寫測試用例,來進行自動化測試。

2、若要多裝置並行,同時執行自動化測試,那麼需要:

  • 確定裝置個數
  • 每個裝置對應一個 appium server 的埠號,並啟動 appium
  • pytest 要獲取到每個裝置的啟動引數,然後執行自動化測試。

3、實現策略

第一步:從裝置池當中,獲取當前連線的裝置。若裝置池為空,則無裝置連線。

第二步:若裝置池不為空,啟動一個執行緒,用來啟動appium server.與裝置個數對應。
起始server埠為4723,每多一個裝置,埠號預設+4

第三步:若裝置池不為空,則啟用多個執行緒,來執行app自動化測試。

4、具體實現步驟

4.1 通過 adb 命令,獲取當前已連線的裝置數、裝置名稱、裝置的安卓版本號。

定義一個 ManageDevices 類。

1. 重啟adb服務。
2. 通過adb devices命令獲取當前平臺中,已連線的裝置個數,和裝置uuid.
3. 通過adb -P 5037 -s 裝置uuid shell getprop ro.build.version.release獲取每一個裝置的版本號。
4. 將所有已連線裝置的裝置名稱、裝置版本號儲存在一個列表當中。
5. 通過呼叫get_devices_info函數,即可獲得4中的列表。

實現的部分程式碼為:

"""
@Title   : app多裝置並行-appium+pytest
@Author  : 檸檬班-小簡
@Email   : lemonban_simple@qq.com
"""
 
class ManageDevices:
    """
       1、重啟adb服務。
       2、通過adb devices命令獲取當前平臺中,已連線的裝置個數,和裝置uuid.
       3、通過adb -P 5037 -s 裝置uuid shell getprop ro.build.version.release獲取每一個裝置的版本號。
       4、將所有已連線裝置的裝置名稱、裝置版本號儲存在一個列表當中。
       5、通過呼叫get_devices_info函數,即可獲得4中的列表。
    """
 
    def __init__(self):
        self.__devices_info = []
        # 重啟adb服務
        self.__run_command_and_get_stout("adb kill-server")
        self.__run_command_and_get_stout("adb start-server")
 
    def get_devices_info(self):
        """
        獲取已連線裝置的uuid,和版本號。
        :return: 所有已連線裝置的uuid,和版本號。
        """
        self.__get_devices_uuid()
        print(self.__devices_info)
        self.__get_device_platform_vesion()
        return self.__devices_info

4.2 定義一個裝置設定池。

裝置啟動引數管理池。
每一個裝置:對應一個啟動引數,以及appium服務的埠號。

1. desired_caps_config/desired_caps.yaml檔案中儲存了啟動引數模板。
2. 從1中的模板讀取出啟動引數。
3. 從裝置列表當中,獲取每個裝置的裝置uuid、版本號,與2中的啟動引數合併。
4. 每一個裝置,指定一個appium伺服器埠號。從4723開始,每多一個裝置,預設遞增4
5. 每一個裝置,指定一個本地與裝置tcp通訊的埠號。從8200開始,每多一個裝置,預設遞增4.
在啟動引數當中,通過systemPort指定。
因為appium服務會指定一個本地埠號,將資料轉發到安卓裝置上。
預設都是使用8200埠,當有多個appium服務時就會出現埠衝突。會導致執行過程中出現socket hang up的報錯。

實現的部分程式碼:

def devices_pool(port=4723,system_port=8200):
    """
    裝置啟動引數管理池。含啟動引數和對應的埠號
    :param port: appium服務的埠號。每一個裝置對應一個。
    :param system_port: appium服務指定的本地埠,用來轉發資料給安卓裝置。每一個裝置對應一個。
    :return: 所有已連線裝置的啟動引數和appium埠號。
    """
    desired_template = __get_yaml_data()
    devs_pool = []
    # 獲取當前連線的所有裝置資訊
    m = ManageDevices()
    all_devices_info = m.get_devices_info()
    # 補充每一個裝置的啟動資訊,以及設定對應的appium server埠號
    if all_devices_info:
        for dev_info in all_devices_info:
            dev_info.update(desired_template)
            dev_info["systemPort"] = system_port
            new_dict = {
                "caps": dev_info,
                "port": port
            }
            devs_pool.append(new_dict)
            port += 4
            system_port += 4
    return devs_pool

特別注意事項:2 個及 2 個以裝置並行時,會遇到裝置 socket hang up 的報錯。

原因是什麼呢:

在 appium server 的紀錄檔當中,有這樣一行 adb 命令:adb -P 5037 -s 08e7c5997d2a forward tcp:8200 tcp:6790

什麼意思呢?

將本地 8200 埠的資料,轉發到安卓裝置的 6790 埠
所以,本地啟動多個 appium server,都是用的 8200 埠,就會出現衝突。

解決方案:

應該設定為,每一個 appium server 用不同的本地埠號,去轉發資料給不同的裝置。
啟動引數當中:新增systemPort= 埠號來設定。
這樣,每個裝置都使用不同的本地埠,那麼可解決此問題。

4.3 appium server 啟停管理 。

(ps 此處可以使用 appium 命令列版,也可以使用桌面版)

  • 在自動化用例執行之前,必須讓 appium server 啟動起來。
  • 在自動化用例執行完成之後,要 kill 掉 appium 服務。這樣才不會影響下一次執行。

程式碼實現如下:

import subprocess
import os
 
from Common.handle_path import appium_logs_dir
 
class ManageAppiumServer:
    """
    appium desktop通過命令列啟動appium服務。
    不同平臺上安裝的appium,預設的appium服務路徑不一樣。
    初始化時,設定appium服務啟動路徑
    再根據給定的埠號啟動appium
    """
 
    def __init__(self,appium_server_apth):
        self.server_apth = appium_server_apth
 
    # 啟動appium server服務
    def start_appium_server(self,port=4723):
        appium_log_path = os.path.join(appium_logs_dir,"appium_server_{0}.log".format(port))
        command = "node {0} -p {1} -g {2} " 
                  "--session-override " 
                  "--local-timezone " 
                  "--log-timestamp & ".format(self.server_apth, port, appium_log_path)
        subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,shell=True).communicate()
 
    # 關閉appium服務
    @classmethod
    def stop_appium(cls,pc,post_num=4723):
        '''關閉appium服務'''
        if pc.upper() == 'WIN':
            p = os.popen(f'netstat  -aon|findstr {post_num}')
            p0 = p.read().strip()
            if p0 != '' and 'LISTENING' in p0:
                p1 = int(p0.split('LISTENING')[1].strip()[0:4])  # 獲取程序號
                os.popen(f'taskkill /F /PID {p1}')  # 結束程序
                print('appium server已結束')
        elif pc.upper() == 'MAC':
            p = os.popen(f'lsof -i tcp:{post_num}')
            p0 = p.read()
            if p0.strip() != '':
                p1 = int(p0.split('n')[1].split()[1])  # 獲取程序號
                os.popen(f'kill {p1}')  # 結束程序
                print('appium server已結束')

4.4 pytest 當中根據不同的啟動引數來執行自動化測試用例

在使用 pytest 執行用例時,是通過 pytest.main()會自動收集所有的用例,並自動執行生成結果。

這種情況下,appium 對談的啟動資訊是在程式碼當中給定的。

以上模式當中,只會讀取一個裝置的啟動資訊,並啟動與裝置的對談。

雖然 fixture 有引數可以傳遞多個裝置啟動資訊,但它是序列執行的。

需要解決的問題的是:

  • 可以傳遞多個裝置的啟動引數,但不是通過 fixture 的引數。
  • 每傳遞一個裝置啟動引數進來,執行一次 pytest.main()

解決方案:

  • 通過 pytest 的命令列引數。即在 pytest.main()的引數當中,將裝置的啟動資訊傳進來。
  • 使用 python 的多執行緒來實現。每接收到一個裝置啟動引數,就啟動一個執行緒來執行 pytest.main

4.4.1 第一個,pytest 的命令列引數。

首先需要在 conftest.py 新增命令列選項,命令列傳入引數”--cmdopt“。

用例如果需要用到從命令列傳入的引數,就呼叫 cmdopt 函數。

def pytest_addoption(parser):
    parser.addoption(
        "--cmdopt", action="store", default="{platformName:'Android',platformVersion:'5.1.1'}",
        help="my devices info"
    )
 
 
@pytest.fixture(scope="session")
def cmdopt(request):
    return request.config.getoption("--cmdopt")
 
 
@pytest.fixture
def start_app(cmdopt):
    device = eval(cmdopt)
    print("開始與裝置 {} 進行對談,並執行測試用例 !!".format(device["caps"]["deviceName"]))
    driver = start_appium_session(device)
    yield driver
    driver.close_app()
    driver.quit()

4.4.2 使用多執行緒實現: 每接收到一個裝置啟動引數,就啟動一個執行緒來執行 pytest.main

定義一個 main.py。

run_case 函數。

此方法主要是:接收裝置啟動引數,通過 pytest.main 去收集並執行用例。

# 根據裝置啟動資訊,通過pytest.main來收集並執行用例。
def run_cases(device):
  """
  引數:device為裝置啟動引數。在pytest.main當中,傳遞給--cmdopt選項。
  """
    print(["-s", "-v", "--cmdopt={}".format(device)])
    reports_path = os.path.join(reports_dir,"test_result_{}_{}.html".format(device["caps"]["deviceName"], device["port"]))
    pytest.main(["-s", "-v",
                 "--cmdopt={}".format(device),
                 "--html={}".format(reports_path)]
                )
每有一個裝置,就啟動一個執行緒,執行 run_cases 方法。
# 第一步:從裝置池當中,獲取當前連線的裝置。若裝置池為空,則無裝置連線。
devices = devices_pool()
 
# 第二步:若裝置池不為空,啟動appium server.與裝置個數對應。起始server埠為4723,每多一個裝置,埠號預設+4
if devices and platform_name and appium_server_path:
    # 建立執行緒池
    T = ThreadPoolExecutor()
    # 範例化appium服務管理類。
    mas = ManageAppiumServer(appium_server_path)
    for device in devices:
        # kill 埠,以免佔用
        mas.stop_appium(platform_name,device["port"])
        # 啟動appium server
        task = T.submit(mas.start_appium_server,device["port"])
        time.sleep(1)
 
    # 第三步:若裝置池不為空,在appium server啟動的情況下,執行app自動化測試。
    time.sleep(15)
    obj_list = []
    for device in devices:
        index = devices.index(device)
        task = T.submit(run_cases,device)
        obj_list.append(task)
        time.sleep(1)
 
    # 等待自動化任務執行完成
    for future in as_completed(obj_list):
        data = future.result()
        print(f"sub_thread: {data}")
 
    # kill 掉appium server服務,釋放埠。
    for device in devices:
        ManageAppiumServer.stop_appium(platform_name, device["port"])

到此這篇關於pytest多執行緒與多裝置並行appium的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com