python和Appium移動端多設(shè)備自動化測試框架實現(xiàn)
前言:
本篇文章主要介紹基于pytest和Appium框架,支持Android和iOS功能自動化的測試框架。同時該框架支持多設(shè)備測試,并利用allure庫,生成可視化測試報告。本框架主要涉及的內(nèi)容包括:python3、pytest、appium、allure等,此處已假設(shè)你具備相應(yīng)的基礎(chǔ)知識,同時已有可以隨時運行的測試環(huán)境(iOS設(shè)備的測試只能在Mac系統(tǒng)中執(zhí)行,沒有Mac的朋友們,可以看看不執(zhí)行)
一、流程圖
本部分內(nèi)容先從自動化測試的整體流程開始介紹,目的是希望大家在開始動手去實現(xiàn)框架之前,對測試過程做到清晰明了,這樣在實現(xiàn)過程中,才能幫助我們無論何時,都不會迷茫和不知所措。才能讓我們知道從何開始,如何優(yōu)化以及拓展。
那么我們先來看下面這張流程圖:
以上是本文所介紹框架的核心流程圖,上圖已經(jīng)展現(xiàn)了框架的核心流程,所以在接下來的講述中,大家可以參考該圖進行理解和優(yōu)化。
二、appium服務(wù)
在開始我們的測試之前,還有很多的工作需要我們?nèi)ヌ幚?,這其中最重要,也是我們開始的第一步,就是開啟appium的本地服務(wù)。關(guān)于appium的實現(xiàn)原理,本文不作過多的講解,小編會抽空進行補充,屆時也希望大家能及時關(guān)注。心急的小伙伴也可以自行百度哦~這里僅介紹啟動服務(wù)的方法。
根據(jù)appium官方的介紹,我們可以通過下面的方式來啟動appium服務(wù):
/usr/local/bin/appium -a ip -p port
也就是我們在啟動appium時,指定ip和端口,一般來說,本地ip使用127.0.0.1即可,官方默認端口為4723,我們也可以修改成自己想要的端口,只要保證使用的端口沒有被其他服務(wù)占用即可。(小技巧:如果你不知道自己appium安裝路徑,可通過which appium來幫你找到)
啟動服務(wù)之后,一般我們可以通過訪問這個連接來驗證服務(wù)是否正常:http://127.0.0.1:4723/wd/hub/status
??烧TL問并返回json格式數(shù)據(jù)時,則說明服務(wù)已正常啟動。
但事實上,并不是每次啟動都可以順利進行,總會有一些意外的情況發(fā)生。比如說端口被占用。遇到這種情況我們也不必驚慌,做好應(yīng)對即可。那么今天我們就上述的過程結(jié)合python,把它實現(xiàn)出來。
上面的過程,用python來實現(xiàn),其實很簡單,我們這里選擇使用python中的subprocess庫來執(zhí)行命令,從而達到我們預(yù)期。
代碼片段如下:
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) """ 待實現(xiàn)的連接設(shè)備方法 """ 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("關(guān)閉被占用的端口號:%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服務(wù)之前,通過socket檢查本地端口是否被占用,若被占用,則先釋放端口,然后再啟動服務(wù),否則直接啟動服務(wù)。
至此,服務(wù)啟動完成,接下來就可以開始連接測試設(shè)備。
三、連接測試設(shè)備
當我們啟動好appium服務(wù)后,就可以開始鏈接測試設(shè)備了。因為我們要同時支持Android和iOS的設(shè)備,所以我們先來定義一個Driver類,用來封裝一些共有屬性及方法,然后讓Android和iOS分別繼承它。
appium對于設(shè)備的連接,官方給我們提供了詳細的方法事例:
# 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()
在以上兩個示例中,我們發(fā)現(xiàn),鏈接設(shè)備使用的都是同一個方法,但不同的設(shè)備需要傳入不同的參數(shù),
下面便是鏈接的關(guān)鍵:
driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
既然我們找到了共性,那么就可以對該部分內(nèi)容進行一番改造,讓它來自動完成一些它可以完成的事情。那么首先,我們來看一下,再鏈接設(shè)備的過程中,我們到底做了些什么。
從上面的代碼不難看出,每臺設(shè)備連接都可以看成兩步:第一步配置連接參數(shù)、第二步請求連接。
那么我們就可以封裝一些類和方法,來完成我們想要分端操作的想法了。其實并不困難,我們可以分別寫兩個類AndroidDriver
和IOSDriver,都繼承自Driver,然后實現(xiàn)設(shè)備連接的方法。
具體實現(xiàn)可參考下面的內(nèi)容:
from Driver import Driver class AndroidDriver(Driver): def __init__(self): self.driver = None def get_desired_caps(self): """ 實現(xiàn)繼承的抽象類方法;獲取鏈接設(shè)備的配置信息 返回設(shè)備配置信息 :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): """ 根據(jù)傳入的port,啟動appium服務(wù) :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
上面的方法主要做了兩件事情,首先收集連接設(shè)備需要的desired_caps
信息,然后是連接設(shè)備。需要注意的是,因為我們這個框架是支持多個測試設(shè)備同時連接的,所有這里我們把收集到的每臺測試設(shè)備的desired_caps
信息放到了一個數(shù)組中,并且在連接設(shè)備的時候,我們通過appium服務(wù)的端口號和數(shù)組下標兩個值,來確定,每臺測試設(shè)備連接的appium服務(wù)。
小提示:一個appium服務(wù)無法同時連接多個手機,但是我們希望能同時連接多個測試手機,并且同時在這連接的多個手機上進行測試,所以我們這里啟動了多個appium服務(wù),并指定了每個啟動的服務(wù)端口號。因此我們只需要將端口號和設(shè)備信息對應(yīng)上即可。
至此,啟動服務(wù)和測試設(shè)備連接的實現(xiàn)就結(jié)束了,接下來就是對元素的操作了。那么我們一起來看一下,關(guān)于Element
的那些事情。
四、元素封裝
眾所周知,元素的操作依賴于元素查找。
舉個常見的例子:我想百度搜索一個關(guān)鍵詞,那么我首先要找到搜索框,才能輸入關(guān)鍵詞,然后找到搜索按鈕,并點擊搜索。這就是我們要做的。
常見的定位元素的方法有:ID、XPATH、CLASSNAME、NAME、PREDICATE等,selenium提供了對應(yīng)的方法,我們這里也不做過多的封裝,大家可以直接使用,也可以像我這樣,把一些常見的定位方式封裝成一個統(tǒng)一的方法,實現(xiàn)如下:
def get_element(self, element_id): """ 獲取指定頁面的元素路徑數(shù)據(jù) :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
大家自己選擇是否進行封裝,正常調(diào)用selenium的方法也是OK噠。
同樣的道理,我們還可以封裝一些常用的操作,比如滑動屏幕,鍵盤操作等。
分端元素操作
因為我們分別接入了Android和iOS,那么它們的操作,各有不同之處,我們可以將各自的特色操作分別集中到一個單獨的AndroidElement類和iOSElement類中,這樣在后面使用的時候,我們直接繼承這兩個類就可以,并且從結(jié)構(gòu)上看,也比較清晰。
比如同樣是滑動屏幕,swipe在Android和iOS系統(tǒng)上的表現(xiàn)就不一致,因此我們就選擇了其他方法:
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'})
以上只是一個小例子,只是想說明,如果有這樣的操作差異,我們可以將它們分開處理,這樣會顯得邏輯更清晰。
有了上面的實現(xiàn),我們就只需要寫測試的腳步就可以。寫腳本部分的內(nèi)容就先略過,不做詳細描述,畢竟不同的業(yè)務(wù)需求場景,都有其獨特的腳本邏輯。凡事萬變不離其宗,元素還是那個元素,操作還是那些操作,就讓大家自己去盡情發(fā)揮吧。
那么,一切準備就緒,就差讓我們的程序跑起來了。接下來就讓我們來看看,如何讓我們的測試同時在多個連接的測試設(shè)備上進行測試。
五、運行
因為我們的測試是通過pytest來執(zhí)行的,所以pytest的所有執(zhí)行參數(shù)都是可以正常使用的。而我們,也只是利用pytest的main函數(shù)來完成本次執(zhí)行。唯一不同的是,為了滿足不同設(shè)備同時進行測試,我們?yōu)槊恳慌_設(shè)備的測試,都創(chuàng)建了一個進程。每一個進程都包含了上述完整的流程。選擇進程而非線程的原因也很簡單,相信大家也都知道,進程和線程的關(guān)系吧,在同一個進程中的線程資源是共享的。而在我們看來,每一臺設(shè)備的測試都應(yīng)該是獨立的、互不干擾的,所以我們選擇進程而非線程。
具體實現(xiàn)如下:
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): """ 執(zhí)行測試用例 :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): """ 獲取測試環(huán)境的信息 :return: """ env = { "測試平臺": self.args[2].get("platformName"), "設(shè)備名稱": self.device_name, "設(shè)備系統(tǒng)版本": self.args[2].get("platformVersion"), "設(shè)備udid": self.args[2].get("udid"), "應(yīng)用名稱": 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): """ 執(zhí)行線程中的任務(wù) :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): """ 開啟測試進程執(zhí)行測試 """ 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("設(shè)備%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("沒有設(shè)備可進行測試,請重新連接設(shè)備后嘗試!") exit(-1) def android_run(): caps = AndroidDriver().get_desired_caps() main(caps) def ios_run(): caps = IOSDriver().get_desired_caps() main(caps)
到此這篇關(guān)于python和Appium移動端多設(shè)備自動化測試框架實現(xiàn)的文章就介紹到這了,更多相關(guān)python和Appium自動化內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于asyncio 異步協(xié)程框架實現(xiàn)收集B站直播彈幕
本文給大家分享的是基于asyncio 異步協(xié)程框架實現(xiàn)收集B站直播彈幕收集系統(tǒng)的簡單設(shè)計,并附上源碼,有需要的小伙伴可以參考下2016-09-09Win10操作系統(tǒng)中PyTorch虛擬環(huán)境配置+PyCharm配置
本文主要介紹了Win10操作系統(tǒng)中PyTorch虛擬環(huán)境配置+PyCharm配置,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-09-09Python while、for、生成器、列表推導(dǎo)等語句的執(zhí)行效率測試
這篇文章主要介紹了Python while、for、生成器、列表推導(dǎo)等語句的執(zhí)行效率測試,本文分別用兩段程序測算出了各語句的執(zhí)行效率,然后總結(jié)了什么情況下使用什么語句優(yōu)先使用的語句等,需要的朋友可以參考下2015-06-06日常整理python執(zhí)行系統(tǒng)命令的常見方法(全)
本文是小編日常整理的些關(guān)于python執(zhí)行系統(tǒng)命令常見的方法,比較全面,特此通過腳本之家這個平臺把此篇文章分享給大家供大家參考2015-10-10Python threading模塊condition原理及運行流程詳解
這篇文章主要介紹了Python threading模塊condition原理及運行流程詳解,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-10-10將Jupyter?Notebook(.ipynb)文件轉(zhuǎn)換為Python(.py)文件的3種方法
大多數(shù)數(shù)據(jù)科學(xué)在線課程都把Jupyter Notebook作為教學(xué)媒介,這是因為初學(xué)者在Jupyter Notebook的單元格中編寫代碼,比編寫包含類和函數(shù)的腳本更容易,這篇文章主要給大家介紹了關(guān)于將Jupyter?Notebook(.ipynb)文件轉(zhuǎn)換為Python(.py)文件的3種方法,需要的朋友可以參考下2023-10-10pandas.DataFrame中提取特定類型dtype的列
本文主要介紹了pandas.DataFrame中提取特定類型dtype的列,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02