網站首頁 編程語言 正文
前言:
本篇文章主要介紹基于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
相關推薦
- 2022-08-31 C語言詳解實現猜數字游戲步驟_C 語言
- 2023-01-17 Python使用鄰接矩陣實現圖及Dijkstra算法問題_python
- 2022-10-14 Go?Ginrest實現一個RESTful接口_Golang
- 2022-08-21 android實現貝塞爾曲線之波浪效果_Android
- 2022-06-08 Spring Cloud Ribbon 負載均衡策略
- 2022-09-22 IO流技術中的File類
- 2022-05-14 jQuery選擇器用法介紹_jquery
- 2023-04-03 C++中using的三種用法舉例詳解_C 語言
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支