日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

python和Appium移動端多設備自動化測試框架實現_python

作者:888米兔 ? 更新時間: 2022-06-24 編程語言

前言:

本篇文章主要介紹基于pytest和Appium框架,支持Android和iOS功能自動化的測試框架。同時該框架支持多設備測試,并利用allure庫,生成可視化測試報告。本框架主要涉及的內容包括:python3、pytest、appium、allure等,此處已假設你具備相應的基礎知識,同時已有可以隨時運行的測試環境(iOS設備的測試只能在Mac系統中執行,沒有Mac的朋友們,可以看看不執行)

一、流程圖

本部分內容先從自動化測試的整體流程開始介紹,目的是希望大家在開始動手去實現框架之前,對測試過程做到清晰明了,這樣在實現過程中,才能幫助我們無論何時,都不會迷茫和不知所措。才能讓我們知道從何開始,如何優化以及拓展。

那么我們先來看下面這張流程圖:?

以上是本文所介紹框架的核心流程圖,上圖已經展現了框架的核心流程,所以在接下來的講述中,大家可以參考該圖進行理解和優化。

二、appium服務

在開始我們的測試之前,還有很多的工作需要我們去處理,這其中最重要,也是我們開始的第一步,就是開啟appium的本地服務。關于appium的實現原理,本文不作過多的講解,小編會抽空進行補充,屆時也希望大家能及時關注。心急的小伙伴也可以自行百度哦~這里僅介紹啟動服務的方法。

根據appium官方的介紹,我們可以通過下面的方式來啟動appium服務:

/usr/local/bin/appium -a ip -p port

也就是我們在啟動appium時,指定ip和端口,一般來說,本地ip使用127.0.0.1即可,官方默認端口為4723,我們也可以修改成自己想要的端口,只要保證使用的端口沒有被其他服務占用即可。(小技巧:如果你不知道自己appium安裝路徑,可通過which appium來幫你找到)

啟動服務之后,一般我們可以通過訪問這個連接來驗證服務是否正常:http://127.0.0.1:4723/wd/hub/status。可正常訪問并返回json格式數據時,則說明服務已正常啟動。

但事實上,并不是每次啟動都可以順利進行,總會有一些意外的情況發生。比如說端口被占用。遇到這種情況我們也不必驚慌,做好應對即可。那么今天我們就上述的過程結合python,把它實現出來。

上面的過程,用python來實現,其實很簡單,我們這里選擇使用python中的subprocess庫來執行命令,從而達到我們預期。

代碼片段如下:

import subprocess
import abc
import socket
class Driver:
	__metaclass__ = abc.ABCMeta
	self._host = '127.0.0.1'
	@abc.abstractmethod
	def connect_appium(self, port, n)
		"""
		待實現的連接設備方法
		"""
		return
	def start_appium(self, port):
		server = self.get_local_server_path()
        host = readConfig.ReadConfig().get_commend("host")
        log_path = root_path + '/result/log'
        cmd = "%s -a %s -p %s" % (server, host, str(port))
        if self.check_port(int(port)):
            subprocess.Popen(cmd, shell=True, stdout=open('%s/AppiumServer%s.log' % (log_path, port), 'w'))
            log.logger.info('%s/AppiumServer%s.log' % (log_path, port))
        else:
            log.logger.info("關閉被占用的端口號:%s" % str(port))
            self.kill_appium()
            log.logger.info("端口釋放完畢!啟動Appium-server,端口號:%s" % str(port))
            subprocess.Popen(cmd, shell=True, stdout=open('%s/AppiumServer%s.log' % (log_path, port), 'w'))
            log.logger.info("Appium日志信息存儲地址: %s/AppiumServer%s.log" % (log_path, port))
    def check_port(self, port):
        """
        檢查端口占用情況
        :param port:
        :return:
        """
        try:
            host = local_read_config.get_commend("host")
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            log.logger.info(s.connect((host, port)))
            s.shutdown(2)
        except OSError:
            log.logger.info("端口:%s 可用" % str(port))
            return True
        else:
            log.logger.info("端口:%s 已被占用" % str(port))
            return False

以上代碼,會在啟動appium服務之前,通過socket檢查本地端口是否被占用,若被占用,則先釋放端口,然后再啟動服務,否則直接啟動服務。

至此,服務啟動完成,接下來就可以開始連接測試設備。

三、連接測試設備

當我們啟動好appium服務后,就可以開始鏈接測試設備了。因為我們要同時支持Android和iOS的設備,所以我們先來定義一個Driver類,用來封裝一些共有屬性及方法,然后讓Android和iOS分別繼承它。

appium對于設備的連接,官方給我們提供了詳細的方法事例:

# Android environment
from appium import webdriver
desired_caps = dict(
    platformName='Android',
    platformVersion='10',
    automationName='uiautomator2',
    deviceName='Android Emulator',
    app=PATH('../../../apps/selendroid-test-app.apk')
)
self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
el = self.driver.find_element_by_accessibility_id('item')
el.click()
# iOS environment
from appium import webdriver
desired_caps = dict(
    platformName='iOS',
    platformVersion='13.4',
    automationName='xcuitest',
    deviceName='iPhone Simulator',
    app=PATH('../../apps/UICatalog.app.zip')
)
self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
el = self.driver.find_element_by_accessibility_id('item')
el.click()

在以上兩個示例中,我們發現,鏈接設備使用的都是同一個方法,但不同的設備需要傳入不同的參數,

下面便是鏈接的關鍵:?

driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)

既然我們找到了共性,那么就可以對該部分內容進行一番改造,讓它來自動完成一些它可以完成的事情。那么首先,我們來看一下,再鏈接設備的過程中,我們到底做了些什么。

從上面的代碼不難看出,每臺設備連接都可以看成兩步:第一步配置連接參數、第二步請求連接。

那么我們就可以封裝一些類和方法,來完成我們想要分端操作的想法了。其實并不困難,我們可以分別寫兩個類AndroidDriver和IOSDriver,都繼承自Driver,然后實現設備連接的方法。

具體實現可參考下面的內容:

from Driver import Driver
class AndroidDriver(Driver):
    def __init__(self):
        self.driver = None

    def get_desired_caps(self):
        """
        實現繼承的抽象類方法;獲取鏈接設備的配置信息
        返回設備配置信息
        :return:desired_caps
        """
        desired_list = []
        package = local_read_config.get_value("ANDROID", "package")
        activity = local_read_config.get_value("ANDROID", "activity")
        devices_info = self.update_devices_info()
        for i in range(len(devices_info)):
            udid = devices_info[i].get("udid")
            device_name = devices_info[i].get("devices_name")
            platform_version = devices_info[i].get("version")
            system_port1 = 8200 + 2 * i
            desired_caps = {
                "platformName": "Android",
                "platformVersion": platform_version,
                "appPackage": package,
                "appActivity": activity,
                "deviceName": device_name,
                "automationName": "uiautomator2",
                "udid": udid,
                "systemPort": system_port1,
                "newCommandTimeout": 3000,
                # "adbExecTimeout": 50000
            }
            desired_list.append(desired_caps)
        return desired_list
    def connect_appium(self, port, n):
        """
        根據傳入的port,啟動appium服務
        :param port:
        :param n:
        :return:
        """
        set_adb_path()
        desired_caps = self.get_desired_caps()
        try:
            self.driver = webdriver.Remote("%s:%s/wd/hub" % (super()._remote_url, str(port)), desired_caps[n])
            return self.driver
        except WebDriverException:
            raise WebDriverException
        except ConnectionError:
            raise ConnectionError

上面的方法主要做了兩件事情,首先收集連接設備需要的desired_caps信息,然后是連接設備。需要注意的是,因為我們這個框架是支持多個測試設備同時連接的,所有這里我們把收集到的每臺測試設備的desired_caps信息放到了一個數組中,并且在連接設備的時候,我們通過appium服務的端口號和數組下標兩個值,來確定,每臺測試設備連接的appium服務。

小提示:一個appium服務無法同時連接多個手機,但是我們希望能同時連接多個測試手機,并且同時在這連接的多個手機上進行測試,所以我們這里啟動了多個appium服務,并指定了每個啟動的服務端口號。因此我們只需要將端口號和設備信息對應上即可。

至此,啟動服務和測試設備連接的實現就結束了,接下來就是對元素的操作了。那么我們一起來看一下,關于Element的那些事情。

四、元素封裝

眾所周知,元素的操作依賴于元素查找。

舉個常見的例子:我想百度搜索一個關鍵詞,那么我首先要找到搜索框,才能輸入關鍵詞,然后找到搜索按鈕,并點擊搜索。這就是我們要做的。

常見的定位元素的方法有:ID、XPATH、CLASSNAME、NAME、PREDICATE等,selenium提供了對應的方法,我們這里也不做過多的封裝,大家可以直接使用,也可以像我這樣,把一些常見的定位方式封裝成一個統一的方法,實現如下:

    def get_element(self, element_id):
        """
        獲取指定頁面的元素路徑數據
        :param element_id: 元素ID
        :return: 獲取的元素對象
        """
        element_type = self.page.get(element_id).get("pathType")
        element_value = self.page.get(element_id).get("pathValue")
        element = None
        if element_type == "ID":
            element = self.driver.find_element_by_id(element_value)
        elif element_type == "CLASSNAME":
            element = self.driver.find_element_by_class_name(element_value)
        elif element_type == "XPATH":
            element = self.driver.find_element_by_xpath(element_value)
        elif element_type == "NAME":
            element = self.driver.find_element_by_name(element_value)
        elif element_type == "ACB_ID":
            element = self.driver.find_element_by_accessibility_id(element_value)
        elif element_type == "PREDICATE":
            element = self.driver.find_element_by_ios_predicate(element_value)
        return element

大家自己選擇是否進行封裝,正常調用selenium的方法也是OK噠。

同樣的道理,我們還可以封裝一些常用的操作,比如滑動屏幕,鍵盤操作等。

分端元素操作

因為我們分別接入了Android和iOS,那么它們的操作,各有不同之處,我們可以將各自的特色操作分別集中到一個單獨的AndroidElement類和iOSElement類中,這樣在后面使用的時候,我們直接繼承這兩個類就可以,并且從結構上看,也比較清晰。

比如同樣是滑動屏幕,swipe在Android和iOS系統上的表現就不一致,因此我們就選擇了其他方法:

AndroidElement:

    def swipe_to_up(self):
        """
        向上劃,頁面滾動到最下方
        :return:
        """
        width = self.driver.get_window_size()["width"]
        height = self.driver.get_window_size()["height"]
        self.driver.swipe(width / 2, height * 3 / 5, width / 2, height / 5, duration=500)

iOSElement:

    def swipe_to_up(self):
        """
        向上滑動
        :return:
        """
        self.driver.execute_script('mobile: swipe', {'direction': 'up'})

以上只是一個小例子,只是想說明,如果有這樣的操作差異,我們可以將它們分開處理,這樣會顯得邏輯更清晰。

有了上面的實現,我們就只需要寫測試的腳步就可以。寫腳本部分的內容就先略過,不做詳細描述,畢竟不同的業務需求場景,都有其獨特的腳本邏輯。凡事萬變不離其宗,元素還是那個元素,操作還是那些操作,就讓大家自己去盡情發揮吧。

那么,一切準備就緒,就差讓我們的程序跑起來了。接下來就讓我們來看看,如何讓我們的測試同時在多個連接的測試設備上進行測試。

五、運行

因為我們的測試是通過pytest來執行的,所以pytest的所有執行參數都是可以正常使用的。而我們,也只是利用pytest的main函數來完成本次執行。唯一不同的是,為了滿足不同設備同時進行測試,我們為每一臺設備的測試,都創建了一個進程。每一個進程都包含了上述完整的流程。選擇進程而非線程的原因也很簡單,相信大家也都知道,進程和線程的關系吧,在同一個進程中的線程資源是共享的。而在我們看來,每一臺設備的測試都應該是獨立的、互不干擾的,所以我們選擇進程而非線程。

具體實現如下:

from multiprocessing import Process
import pytest
import time
import os, re
import subprocess
from appiums.common import read_files
from appiums.driver.iOSDriver import IOSDriver
from driver.androidDriver import AndroidDriver
from driver import Driver
from elements import Element

class Run(Process):
    def __init__(self, name, args):
        super(Run, self).__init__()
        self.name = name
        self.args = args
        self.root_path = os.getcwd()
        self.device_name = re.sub('[\']', '', str(args[2].get("deviceName")).replace(" ", "_"))
    def run_test(self):
        """
        執行測試用例
        :return:
        """
        pytest.main([
                     '--alluredir', '%s/result/data/%s' % (self.root_path, self.device_name)])
        time.sleep(2)
    def generate_report(self):
        """
        整合測試報告到項目根目錄下的result/report目錄下
        :return: none
        """
        cmd = "allure generate %s/result/data/%s -o %s/result/report/%s --clean" \
              % (self.root_path, self.device_name, self.root_path, self.device_name)
        stdout = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, text=True)
        log.logger.info("測試報告查看路徑:%s" % str(stdout.stdout.readlines()[0]).split(" ")[-1][:-1])
    def get_environment_info(self):
        """
        獲取測試環境的信息
        :return:
        """
        env = {
            "測試平臺": self.args[2].get("platformName"),
            "設備名稱": self.device_name,
            "設備系統版本": self.args[2].get("platformVersion"),
            "設備udid": self.args[2].get("udid"),
            "應用名稱": self.args[2].get("bundleId") if str(self.args[2].get("platformName")).lower() == 'ios' else self.args[2].get("appPackage"),
        }
        return env
    def run(self):
        """
        執行線程中的任務
        :return:
        """
        Driver.Driver().start_appium(self.args[0])
        time.sleep(5)
        self.set_driver()
        time.sleep(1)
        self.run_test()
        time.sleep(1)
        read_files.set_environment(self.device_name, self.get_environment_info())
        time.sleep(1)
        self.generate_report()

def main(desired_caps):
    """
    開啟測試進程執行測試
    """
    list_p = []
    process_num = len(desired_caps)
    if process_num > 0:
        for a in range(process_num):
            port1 = 4723 + 2 * a
            p = Run('測試進程-%s' % str(port1), args=(port1, a, desired_caps[a]))
            p.start()
            log.logger.info("設備%s在進程 %s 上進行測試, 進程ID:%s" % (desired_caps[a].get("deviceName"), p.name, p.pid))
            list_p.append(p)
        for b in list_p:
            b.join()
        Driver.Driver().kill_appium()
    else:
        log.logger.error("沒有設備可進行測試,請重新連接設備后嘗試!")
        exit(-1)

def android_run():
    caps = AndroidDriver().get_desired_caps()
    main(caps)

def ios_run():
    caps = IOSDriver().get_desired_caps()
    main(caps)

原文鏈接:https://my.oschina.net/u/3041656/blog/5146434

欄目分類
最近更新