那么多自动化app的工具为什么选则appium?
android版

IOS版


大家看看这幅图, 包含了 3个主体部分 : 自动化程序、Appium Server、移动设备
自动化程序
自动化程序是由我们来开发的,实现具体的 手机自动化 功能。
要发出具体的指令控制手机,也需要使用 客户端库。
和Selenium一样,Appium 组织 也提供了多种编程语言的客户端库,包括 java,python,js, ruby等,方便不同编程语言的开发者使用。
我们需要安装好客户端库,调用这些库,就可以发出自动化指令给手机。
Appium Server
Appium Server 是 Appium 组织开发的程序,它负责管理手机自动化环境,并且转发 自动化程序的控制指令 给 手机,并且转发 手机给 自动化程序的响应消息。
手机设备
我们这里说的手机设备,其实不仅仅是手机,包括所有 苹果、安卓的移动设备,比如:手机、平板、智能手表等。
为了直观方便的讲解,这里我们简称: 手机
当然手机上也包含了 我们要自动化控制的 手机应用APP。
手机设备为什么能 接收并且处理自动化指令呢?
因为,Appium Server 会在手机上 安装一个 自动化代理程序, 代理程序会等待自动化指令,并且执行自动化指令
比如:要模拟用户点击界面按钮,Appium 自动化系统的流程是这样的:
自动化程序 调用客户端库相应的函数, 发送 点击元素 的指令(封装在HTTP消息里)给 Appium Server
Appium Server 再转发这个指令给 手机上的自动化代理
手机上的自动化代理 接收到 指令后,调用手机平台的自动化库,执行点击操作,返回点击成功的结果给 Appium Server
Appium Server 转发给 自动化程序
自动化程序了解到本操作成功后,继续后面的自动化流程
其中,自动化代理控制,使用的什么库来实现自动化的呢?
如果测试的是苹果手机, 用的是苹果的 XCUITest 框架 (IOS9.3版本以后)
如果测试的是安卓手机,用的是安卓的 UIAutomator 框架 (Android4.2以后)
这些自动化框架提供了在手机设备上运行的库,可以让程序调用这些库,像人一样自动化操控设备和APP,比如:点击、滑动,模拟各种按键消息等。
安装包在为了方便我放在阿里云盘上面了是地址:https://sharelinkpre.rongdasoft.com/share-link/index.html?q=158e744d8a404000
步骤如下
<!-- https://mvnrepository.com/artifact/io.appium/java-client -->
<dependency>
<groupId>io.appium</groupId>
<artifactId>java-client</artifactId>
<version>6.1.0</version>
</dependency>
安装appium Server
appium Server是用nodejs运行的,基于js开发出来的.Appium组织为了方便大家安装使用制作了一个可执行程序appiumDesktoop
把nodejs运行环境,appiumServer和一些工具打包在了里面了,只需要简单的下载安装就可以了
「Appium-windows-1.15.1.exe」https://www.aliyundrive.com/s/vqSgSGpw78y
安装AndroidJDK
百度androidSDK进行下载,下载完毕后配置ANDROID_HOME,配置adb命令monkey命令等地址即可
安装javaSDK
需要用到androidSDK而androidSDK需要用到JAVA的JDK环境.所以在这里也需要安装一下java的JDK
访问地址:配置JAVA_HOME和bin和jre\bin即可:「jdk-8u211-windows-x64.exe」https://www.aliyundrive.com/s/FSMV3bbxCvd
连接手机/模拟器
上述环境都准备好,要自动化手机app需要,在你运行程序的电脑上用USB线或者wifi连接上你的安卓手机,进入手机设置->关于手机,不断点击版本号菜单,推出上级菜单,就会出现一个开发者模式设置按钮进入后启动USB调试即可
连接好以后输入adb devices -l 命令来列出连接在电脑上的安卓设备.,点击回车如果输出一下内容则表示连接成功可以进行自动化了
注意:输入这个可以获取当前正在运行app的报名和activity: adb shell dumpsys window | findstr mCurrentFocus
from appium import webdriver
from selenium.webdriver.common.by import By
from appium.webdriver.extensions.android.nativekey import AndroidKey
desired_caps = {
'platformName': 'Android', # 被测手机是安卓
'platformVersion': '8', # 手机安卓版本
'deviceName': 'xxx', # 设备名,安卓手机可以随意填写
'appPackage': 'tv.danmaku.bili', # 启动APP Package名称
'appActivity': '.ui.splash.SplashActivity', # 启动Activity名称
'unicodeKeyboard': True, # 使用自带输入法,输入中文时填True
'resetKeyboard': True, # 执行完程序恢复原来输入法
'noReset': True, # 不要重置App
'newCommandTimeout': 6000,
'automationName' : 'UiAutomator2'
# 'app': r'd:\apk\bili.apk',
}
# 连接Appium Server,初始化自动化环境
driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
# 设置缺省等待时间
driver.implicitly_wait(5)
# 如果有`青少年保护`界面,点击`我知道了`
iknow = driver.find_elements(By.ID, "text3")
if iknow:
iknow.click()
# 根据id定位搜索位置框,点击
driver.find_element(By.ID, 'expand_search').click()
# 根据id定位搜索输入框,点击
sbox = driver.find_element(By.ID, 'search_src_text')
sbox.send_keys('开心的')
# 输入回车键,确定搜索
driver.press_keycode(AndroidKey.ENTER)
# 选择(定位)所有视频标题
eles = driver.find_elements(By.ID, 'title')
for ele in eles:
# 打印标题
print(ele.text)
input('**** Press to quit..')
driver.quit()
注意:Appium Python 现在已经升级到 2.x 大版本,依赖 Selenium 4 以后, 下面这种 find_element_by* 方法都作为过期不赞成的写法
driver.find_element_by_id('username').send_keys('byhy')
运行会有告警,都要写成下面这种格式
from selenium.webdriver.common.by import By
wd.find_element(By.ID, 'username').send_keys('byhy')
而后续还有 Appium独有的查找方式,比如
driver.find_element_by_accessibility_id('byhy')
driver.find_element_by_android_uiautomator(code)
要改成
from appium.webdriver.common.appiumby import AppiumBy
driver.find_element(AppiumBy.ACCESSIBILITY_ID, 'byhy')
driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, code)
# 这样也可以根据ID
wd.find_element(AppiumBy.ID, 'username').send_keys('byhy')
从示例代码,大家就可以发现,和Selenium Web自动化一样,要操作界面元素,必须先 定位(选择)元素。
Appium是基于Selenium的,所以 和 Selenium 代码 定位元素的 基本规则相同:
做 Selenium Web 自动化的时候,要找到元素,我们是通过浏览器的开发者工具栏来查看元素的特性,根据这些特性(属性和位置),来定位元素
Appium 要自动化手机应用,同样需要工具查看界面元素的特征。
常用的查看工具是: Android Sdk包中的 uiautomateviewer 和 Appium Desktop 中的 Appium Inspector
如:resource-id com.phi.letter.letterphi:id/data_title
其中data_title就是该元素的ID
from appium.webdriver.common.appiumby import AppiumBy
eles = driver.find_elements(AppiumBy.ID, 'data_title')
from appium.webdriver.common.appiumby import AppiumBy
driver.find_elements(AppiumBy.CLASS_NAME,'android.widget.TextView')[3].click()
from appium.webdriver.common.appiumby import AppiumBy
driver.find_element(AppiumBy.ACCESSIBILITY_ID, '找人')
from appium.webdriver.common.appiumby import AppiumBy
driver.find_element(AppiumBy.XPATH, '//ele1/ele2[@attr="value"]')
注意:
selenium自动化中, xpath表达式中每个节点名是html的tagname。
但是在appium中, xpath表达式中 每个节点名 是元素的class属性值。
比如:要选择所有的文本节点,就使用如下代码
# 定位text
driver.find_element_by_xpath("//*[@text='扫一扫']").click()
# 定位 resource-id
driver.find_element_by_xpath("//*[@resource-id='com.taobao.taobao:id/tv_scan_text']").click()
# 也可以联合@resource-id属性和@text文本属性来下定位
driver.find_element_by_xpth("//*[@resource-id='com.taobao.taobao:id/tv_scan_text'][@text='扫一扫']").click()
# 定位搜索框 //class属性
driver.find_element_by_xpath("//android.widget.EditText").click()
# 定位搜索框 //*[@class='class属性']
driver.find_element_by_xpath("//*[@class='android.widget.EditText']").click()
dd=driver.find_element(AppiumBy.XPATH,'//androidx.recyclerview.widget.RecyclerView/android.view.ViewGroup[1]/android.widget.TextView[1]')# xpath是从1开始的
print(dd.text)
contains模糊定位
# contains匹配text
driver.find_element_by_xpath('//*[contains(@text, "注册/登录")]').click()
time.sleep(3)
# contains匹配textcontent-desc
driver.find_element_by_xpath("//*[contains(@content-desc, '帮助')]").click()
根据id,classname, accessibilityid,xpath,这些方法选择元素,其实底层都是利用了安卓 uiautomator框架的API功能实现的。
参考 这里的谷歌安卓官方文档介绍:https://developer.android.google.cn/training/testing/ui-automator
也就是说,程序的这些定位请求,被Appium server转发给手机自动化代理程序,就转化为为uiautomator里面相应的定位函数调用。
其实,我们的自动化程序,可以直接告诉 手机上的自动化代理程序,让它 调用UI Automator API的java代码,实现最为直接的自动化控制。
主要是通过 UiSelector 这个类里面的方法实现元素定位的,比如
code = 'new UiSelector().text("业务").className("android.widget.TextView")'
ele=driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, code)
ele.click()
就是通过 text 属性 和 className的属性 两个条件 来定位元素
UiSelector里面有些元素选择的方法 可以解决 前面解决不了的问题。
比如
text 方法
可以根据元素的文本属性查找元素
textContains
根据文本包含什么字符串
textStartsWith
根据文本以什么字符串开头
textmartch 方法
可以使用正则表达式 选择一些元素,如下
code = 'new UiSelector().textMatches("^我的.*")'
UiSelector 的 instance 和 index 也可以用来定位元素,都是从0开始计数, 他们的区别:
instance是匹配的结果所有元素里面 的第几个元素
index则是其父元素的几个节点,类似xpath 里面的*[n]
UiSelector 的 childSelector 可以选择后代元素,比如
code = 'new UiSelector().resourceId("tv.danmaku.bili:id/recycler_view").childSelector(new UiSelector().className("android.widget.TextView"))'
ele = driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, code)
注意: childSelector后面的引号要框住整个 子 uiSelector 的表达式
目前有个bug:只能找到符合条件的第一个元素,参考appium 在github上的 issues:
https://github.com/appium/java-client/issues/150
from appium.webdriver.extensions.android.nativekey import AndroidKey
# 输入回车键,确定搜索
driver.press_keycode(AndroidKey.ENTER)
按键的定义,可以参考这篇文档 https://github.com/appium/python-client/blob/master/appium/webdriver/extensions/android/nativekey.py
from appium.webdriver.common.touch_action import TouchAction
# ...
actions = TouchAction(driver)
actions.long_press(element)
actions.perform()
driver.open_notifications()
或者使用滑动
driver.swipe(start_x=10, start_y=0, end_x=10, end_y=1000, duration=500)
收起通知栏
收起通知栏,可以使用前面介绍的模拟按键, 发出返回按键 的方法
driver.back()
driver.keyevent(4)
我们自动化过程中,可能需要截屏手机,并且下载到指定目录中,就可以在我们的Python程序中这样写
import os
os.system('adb shell screencap /sdcard/screen3.png && adb pull /sdcard/screen3.png')
特别是,还可以通过adb 使用 am(activity manager) 和pm (package manager) 两个工具, 可以启动 Activity、强行停止进程、广播 intent、修改设备屏幕属性、列出应用、卸载应用等。
使用 driver.terminate_app(appPackage) 即可
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 找到 webview 对象
WebView myWebView = (WebView) findViewById(R.id.jcywebview);
// 添加打开 webview内容debug开关
myWebView.setWebContentsDebuggingEnabled(true);
};
然后,编译出一个支持自动化的版本。
否则无法对webview中的内容进行自动化操作。
但是,因为网页内容在手机中呈现,不能像电脑浏览器那样,打开开发者工具,查看元素内容。
那我们怎么查看元素的属性特征,来定位选择元素呢?
这里面要分两种情况:
webview不依赖App环境
这种情况:App的内嵌 webview 和 native部分没有交互 的。
webview 只是打开一个固定的网址(一般是该公司的手机网址)而已。
这种情况,我们要查看webview内容非常简单。
因为其实和手机没有关系。
直接用Chrome浏览器F12里面的手机模式打开对应的网页,即可。
webview依赖App环境
首先,确保我们的应用运行,然后应用访问webview页面,打开电脑浏览器地址栏输入 chrome://inspect ,出现如下所示界面

(括号中说的是webview版本号)
webview版本也比较老的,就会有问题, 尽量使用新手机进行自动化
点击 inspect即可查看
webview自动化代码 和 电脑上浏览器的自动化 基本差不多。
但是有一点要注意:
手机App中 webview里面的网页内容, 是在一个独立于应用native部分的环境里面的。
而缺省情况下,find_element_by_xxx 这样的代码选择元素, Appium 只会在 native 部分的界面寻找元素。 肯定找不到元素。
Appium 把一个界面环境 称之为一个 context 。
native 部分的 context 名字为 NATIVE_APP , 而webview部分的context则为 WEBVIEW_XXX (XXX部分是 应用的 app package名)
我们怎么查看当前有哪些context呢?
我们的代码通过 driver 对象的 contexts 属性来获取,也就是 driver.contexts。
driver 对象的 current_context 属性对应当前的 context 对象。
大家可以打开自动化代码, 添加如下内容
print(driver.contexts)
执行一下,解释一下,可以发现结果如下。
['NATIVE_APP', 'WEBVIEW_stetho_com.phi.letter.letterphi']
我们的应用中, webview 的 context 就是 WEBVIEW_stetho_com.phi.letter.letterphi
而当前的context 是’NATIVE_APP', 所以当前的自动操作都是在native context里面的进行的。
要对该webview里面的网页内容进行自动化操作,必须先将当前的context切换为 webview的context,怎么切呢?
使用 switch_to.context
driver.switch_to.context('WEBVIEW_stetho_com.phi.letter.letterphi')
使用该语句如果webview版本和chrome驱动版本不一致则会报版本错误问题此时我们需要查看报错的版本,下载对应的chrome版本驱动包即可
版本对应查看地址:https://raw.githubusercontent.com/appium/appium-chromedriver/master/config/mapping.json
chrome驱动包下载地址:http://chromedriver.storage.googleapis.com/index.html



好,现在我们修改代码,如下所示
from selenium.webdriver.common.by import By
driver.switch_to.context('WEBVIEW_com.example.jcy.wvtest')
driver.find_element(By.ID, 'index-kw').send_keys('开心的小哈')
driver.find_element(By.ID, 'index-bn').click()
执行一下,发现可以自动化了
那么怎么切换回 native app 进行自动化呢?
当然是 继续使用 switch_to.context ,如下
driver.switch_to.context('NATIVE_APP')
有的公司开发了手机版网站,直接用手机浏览器打开的,比如,xiaomi 京东等。
并不是 手机App
这种手机网页,我们怎么 程序自动化呢?
首先,必须在手机上安装谷歌浏览器。
以百度为例,
首先启动 Appium Desktop。
然后,我们的自动化程序代码如下所示:
from appium import webdriver
from selenium.webdriver.common.by import By
desired_caps = {}
desired_caps['platformName'] = 'Android'
desired_caps['platformVersion'] = '7'
desired_caps['deviceName'] = 'test'
desired_caps['browserName'] = 'Chrome'
desired_caps['newCommandTimeout'] = 6000
driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
driver.implicitly_wait(10)
driver.get('http://www.baidu.com')
driver.find_element(By.ID, 'index-kw').send_keys('开心的小哈')
driver.find_element(By.ID, 'index-bn').click()
driver.quit()
更多了解:AndroidViewClient UiAutomator2
raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.InvalidArgumentException: Message: invalid argument: invalid locator
(Session info: chrome=88.0.4324.93)
Stacktrace:
Backtrace:
GetHandleVerifier [0x000BB963+483]
GetHandleVerifier [0x000BB941+449]
GetHandleVerifier [0x00463308+3832712]
GetHandleVerifier [0x004863A7+3976231]
GetHandleVerifier [0x004A8822+4116642]
GetHandleVerifier [0x0049B43A+4062394]
GetHandleVerifier [0x004A7159+4110809]
GetHandleVerifier [0x0049B2EB+4062059]
GetHandleVerifier [0x0047ED14+3945876]
GetHandleVerifier [0x0047FBCE+3949646]
GetHandleVerifier [0x0047FB59+3949529]
Ordinal0 [0x0007B5CC+46540]
Ordinal0 [0x00079F53+40787]
Ordinal0 [0x00079B12+39698]
GetHandleVerifier [0x00381468+2907368]
GetHandleVerifier [0x001C71EE+1096302]
GetHandleVerifier [0x00183E8D+821005]
GetHandleVerifier [0x0018396B+819691]
GetHandleVerifier [0x00183881+819457]
GetHandleVerifier [0x001AF463+998627]
BaseThreadInitThunk [0x76B4FA29+25]
RtlGetAppContainerNamedObjectPath [0x77B57A7E+286]
RtlGetAppContainerNamedObjectPath [0x77B57A4E+238]
(No symbol) [0x00000000]
问题原因导致:如果 chromedriver 在 W3C 模式下工作,则会出现这种情况。W3C标准只声明CSS和XPATH定位器,其中id和名称的位置已被删除为过时,因为CSS涵盖了它们。
有三种可能的解决方法:
desired_caps['chromeOptions'] = {'w3c': False}
desired_caps['showChromedriverLog'] = data['showChromedriverLog']
即可;
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po