前言:
前段时间学习Android自动化测试,网上关于Appium的介绍和学习资料比较散乱,大多不齐全,官网API看的也是一脸懵,所以决定自己整理一份文档,供自己学习,也希望能帮助到正在学习Appium和自动化测试的朋友们。
在本地写了一个md文档之后,放到了GitHub上维护。鉴于的颜值,我决定转战写博客,但是的Markdown貌似不只是页内锚点跳转,所以本文的所有锚点都无效了。文章有些长,需要pdf的朋友们可以去我的GitHub上拿。
为什么选择Appium
学习Android自动化测试的时候,看了google官方推荐的几个工具和测试框架,比如Espresso和UI Automator,其中在使用UI Automator来做UI自动化测试的时候,发生一点问题:向控件输入字符的时候无效!不管是中文还是英文都无法输入,而且最终竟然还显示测试通过。经过多方搜索都找不到解决办法,最终还是选择Appium,并且Appium是跨平台的,可以测试ios和Android还有webApp;其中Android平台上是基于UI Automator的,所以可以无缝切换使用UI Automator的API。
作者:GeeJoe
邮箱:[email protected]
GitHub:https://github.com/GeeJoe/AppiumDoc
更新时间:2017-09-14 16:44
Appium使用指南
- 环境配置
- 启动Appium Server
- 学习DesiredCapabilities
- Session简介
- 元素定位
- 使用UI Automator定位元素
- 使用JUnit组织测试用例
- 显式等待和隐式等待
环境配置
- 安装Nodejs
- 安装Appium
- 配置IDE
安装NodeJs
到 Nodejs官网 下载最新版本的NodeJs并直接安装。安装完毕后,打开命令行,输入 node -v
,出现类似下面的信息说明安装成功。
V4.0.0
安装Appium
到 Appium 官网 载和你所使用系统一致的版本进行安装。
验证安装
当确认Appium安装完毕后,我们可以通过 appium-doctor
的命令来检查当前appium安装是否完善,当前的JDK、SDK等环境是否配置正确。
如果 appium-doctor
返回的内容是有错的,请根据返回的具体的提示,将你的环境搭建完善。
如果返回的结果类似如下,说明安装成功
...
...
Android Checks were successful.
All Checks were successful
需要注意的是,如果你是通过安装包安装的,使用 appium-doctor 命令时必须切换到
C:\Program Files (x86)\Appium\node_modules.bin 目录;为了方便,需要把.bin目录添加到环境变量Path中。
配置IDE
配置AndroidStudio
- 新建一个项目,AndroidStudio新建项目会自动生成一个 androidTest 包和 test 包
- 下载 Appium java client 及 Selenium Java standalone server 两个库对应的 jar 包
- 将两个jar包复制到项目module里的 libs 目录
- 选择两个jar包,单击鼠标右键,选择
Add as Library
- 配置成功,可以在 test 包中新建测试用例了
启动Appium Server
通过图形化界面启动
双击Appium图标,打开Appium应用
点击箭头所指的按钮即可启动
从命令行启动
只需要一个命令:
appium
若显示如下信息则启动成功:
info: Welcome to Appium v1.4.16 (REV ae6877eff263066b26328d457bd285c0cc62430d)
info: Appium REST http interface listener started on 0.0.0.0:4723
info: Console LogLevel: debug
若需要退出,按下 ctrl+c
即可
配置启动参数
启动时可以手动配置参数,了解所有参数,可查阅 Appium 官方文档
标志 | 默认值 | 描述 | 例子 |
---|---|---|---|
-a , --address | 0.0.0.0 | 监听的 ip 地址。注意在图形化界面上默认值为 127.0.0.1 | --address 0.0.0.0 |
-p , --port | 4723 | 监听的端口。需要启动多个 appium server 进行并行测试时需要保证每个server 的监听端口不一样。 | --port 4723 |
--log-timestamp | false | 在日志输出里显示时间戳 | |
--local-timezone | false | 在日志输出的时间戳使用本地时间 | |
-g , --log | null | 将日志输出到指定文件 | --g /path/to/appium.log |
--session-override | false | 允许 session 被覆盖 (冲突的话) | |
--command-timeout | 60 | 默认所有会话的接收命令超时时间 (在超时时间内没有接收到新命令,自动关闭会话)。 会被新的超时时间覆盖 | |
这些参数同样可以在图形界面设置:
学习DesiredCapabilities
DesiredCapabilities简介
DesiredCapabilities 携带了一些配置信息,在启动session的时候是必须提供,如启动模式、apk/app配置、package/activity配置、浏览器配置、键盘配置等。
Desired Capabilities关键字
Desired Capabilities的重要作用是在启动时传递信息给Appium Server。
下表中列举了Appium常用的 Desired Capabilities 关键字。
键 | 描述 | 值 |
---|---|---|
automationName |
自动化测试的引擎 | Appium (默认)或者 Selendroid |
platformName |
使用的手机操作系统 | iOS , Android , 或者 FirefoxOS |
platformVersion |
手机操作系统的版本 | 例如 7.1 , 4.4 |
deviceName |
使用的手机或模拟器类型 | iPhone Simulator , iPad Simulator , iPhone Retina 4-inch , Android Emulator , Galaxy S4 , 等等.... 在 iOS 上,使用 Instruments 的 instruments -s devices 命令可返回一个有效的设备的列表。在 Andorid 上虽然这个参数目前已被忽略,但仍然需要添加上该参数 |
app |
本地绝对路径或远程 http URL 所指向的一个安装包(.ipa ,.apk ,或 .zip 文件)。Appium 将其安装到合适的设备上。请注意,如果您指定了 appPackage 和 appActivity 参数(见下文),Android 则不需要此参数了。该参数也与 browserName 不兼容。 |
/abs/path/to/my.apk 或 http://myapp.com/app.ipa |
browserName |
做自动化时使用的浏览器名字。如果是一个应用则只需填写个空的字符串 | 'Safari' 对应 iOS,'Chrome', 'Chromium', 或 'Browser' 则对应 Android |
newCommandTimeout |
用于客户端在退出或者结束 session 之前,Appium 等待客户端发送一条新命令所花费的时间(秒为单位) | 例如 60 |
language |
(Sim/Emu-only) 为模拟器设置语言 | 例如 fr |
locale |
(Sim/Emu-only) 为模拟器设置所在区域 | 例如 fr_CA |
udid |
连接真机的唯一设备号 | 例如 1ae203187fc012g |
orientation |
(Sim/Emu-only) 模拟器当前的方向 | 竖屏 或 横屏 |
autoWebview |
直接转换到 Webview 上下文(context)。默认值为 false |
true , false |
noReset |
在当前 session 下不会重置应用的状态。默认值为 false |
true , false |
fullReset |
(iOS)删除所有的模拟器文件夹。(Android) 要清除 app 里的数据,请将应用卸载才能达到重置应用的效果。在 Android, 在 session 完成之后也会将应用卸载掉。默认值为 false |
true , false |
Android 独有
键 | 描述 | 值 |
---|---|---|
appActivity |
Activity 的名字是指从你的包中所要启动的 Android acticity。他通常需要再前面添加. (例如 使用 .MainActivity 代替 MainActivity ) |
MainActivity , .Settings |
appPackage |
运行的 Android 应用的包名 | com.example.android.myApp , com.android.settings |
appWaitActivity |
用于等待启动的 Android Activity 名称 | SplashActivity |
appWaitPackage |
用于等待启动的 Android 应用的包 | com.example.android.myApp , com.android.settings |
appWaitDuration |
用于等待 appWaitActivity 启动的超时时间(以毫秒为单位)(默认值为 20000 ) |
30000 |
deviceReadyTimeout |
用于等待模拟器或真机准备就绪的超时时间 | 5 |
androidCoverage |
用于执行测试的 instrumentation 类。 传送 -w 参数到如下命令 adb shell am instrument -e coverage true -w |
com.my.Pkg/com.my.Pkg.instrumentation.MyInstrumentation |
enablePerformanceLogging |
(仅适用于 Chrome 与 webview)开启 Chromedriver 的性能日志。(默认值为 false ) |
true , false |
androidDeviceReadyTimeout |
用于等待设备在启动应用后准备就绪的超时时间。以秒为单位。 | 例如 30 |
androidInstallTimeout |
用于等待在设备中安装 apk 所花费的时间(以毫秒为单位)。默认值为 90000 |
例如 90000 |
adbPort |
用来连接 ADB 服务器的端口(默认值为 5037 ) |
5037 |
androidDeviceSocket |
开发工具的 socket 名称。只有在被测应用是一个使用 Chromium 内核的浏览器时才需要。socket 会被浏览器打开,然后 Chromedriver 把它作为开发者工具来进行连接。 | 例如 chrome_devtools_remote |
avd |
被启动 avd 的名字 | 例如 api19 |
avdLaunchTimeout |
用于等待 avd 启动并连接 ADB 的超时时间(以毫秒为单位),默认值为 120000 。 |
300000 |
avdReadyTimeout |
用于等待 avd 完成启动动画的超时时间(以毫秒为单位),默认值为 120000 。 |
300000 |
avdArgs |
启动 avd 时使用的额外参数 | 例如 -netfast |
useKeystore |
使用自定义的 keystore 给 apk 签名,默认值为 false |
true 或false |
keystorePath |
自定义 keystore 的路径, 默认路径为 ~/.android/debug.keystore | 例如 /path/to.keystore |
keystorePassword |
自定义 keystore 的密码 | 例如 foo |
keyAlias |
key 的别名 | 例如 androiddebugkey |
keyPassword |
key 的密码 | 例如 foo |
chromedriverExecutable |
webdriver 可执行文件的绝对路径(如果 Chromium 内嵌一个自己提供的 webdriver,则应使用他去替换掉 Appium 自带的 chromedriver) | /abs/path/to/webdriver |
autoWebviewTimeout |
用于等待 Webview 上下文(context)激活的时间(以毫秒为单位)。默认值为 2000 |
例如 4 |
intentAction |
用于启动 activity 的 intent action(默认值为 android.intent.action.MAIN ) |
例如 android.intent.action.MAIN , android.intent.action.VIEW |
intentCategory |
用于启动 activity 的 intent category。(默认值为 android.intent.category.LAUNCHER ) |
例如 android.intent.category.LAUNCHER , android.intent.category.APP_CONTACTS |
intentFlags |
用于启动 activity 的标识(flags)(默认值为 0x10200000 ) |
例如 0x10200000 |
optionalIntentArguments |
用于启动 activity 的额外 intent 参数。请查看 Intent 参数 | 例如 --esn , --ez , 等等。 |
dontStopAppOnReset |
在使用 adb 启动应用之前,不要终止被测应用的进程。如果被测应用是被其他钩子(anchor)应用所创建的,设置该参数为 false 后,就允许钩子(anchor)应用的进程在使用 adb 启动被测应用期间仍然存在。换而言之,设置 dontStopAppOnReset 为 true 后,我们在 adb shell am start 的调用中不需要包含 -S 标识(flag)。忽略该 capability 或 设置为 false 的话,就需要包含 -S 标识(flag)。默认值为 false |
true 或false |
unicodeKeyboard |
使用 Unicode 输入法。 默认值为 false |
true 或false |
resetKeyboard |
在设定了 unicodeKeyboard 关键字的 Unicode 测试结束后,重置输入法到原有状态。如果单独使用,将会被忽略。默认值为 false |
true 或false |
noSign |
跳过检查和对应用进行 debug 签名的步骤。仅适用于 UiAutomator,不适用于 selendroid。 默认值为 false |
true 或false |
ignoreUnimportantViews |
调用 uiautomator 的函数 setCompressedLayoutHierarchy() 。由于 Accessibility 命令在忽略部分元素的情况下执行速度会加快,这个关键字能加快测试执行的速度。被忽略的元素将不能够被找到,因此这个关键字同时也被实现成可以随时改变的 设置 ( settings )。 默认值为 false |
true 或 false |
disableAndroidWatchers |
禁用 android 监视器(watchers)。监视器用于监视应用程序的无响应状态(anr)和崩溃(crash),禁用会降低 Android 设备或模拟器的 CPU 使用率。该 capability 仅在使用 UiAutomator 时有效,不适用于 selendroid,默认设置为 false 。 |
true 或 false |
chromeOptions |
允许对 ChromeDriver 传 chromeOptions 的参数。了解更多信息请查阅 chromeOptions | chromeOptions: {args: ['--disable-popup-blocking']} |
recreateChromeDriverSessions |
当移除非 ChromeDriver webview时,终止掉 ChromeDriver 的 session。默认设置为 false |
true 或false |
nativeWebScreenshot |
在 web 的上下文(context),使用原生(native)的方法去截图,而不是用过代理的 ChromeDriver。默认值为 false |
true 或false |
androidScreenshotPath |
在设备中截图被保存的目录名。默认值为 /data/local/tmp |
例如 /sdcard/screenshots/ |
autoGrantPermissions |
让Appium自动确定您的应用需要哪些权限,并在安装时将其授予应用。默认设置为 false |
true 或false |
Session简介
Session 是指我们的测试脚本从打开应用到最终执行完毕关闭应用的整个过程。
对于测试脚本,从申请到退出一个 session 的整个过程如下:
//打开一个应用,也可以称为“申请一个Session”
AndroidDriver driver = new AndroidDriver<>(new URL("http://127.0.0.1:4723/wd/hub"), capabilities);
//此处的所有代码处于一个Session中
...
//关闭应用,也可以称为“退出一个Session”
driver.quit();
元素定位
方法名 | 参数 | 描述 |
---|---|---|
findElementByName | String 元素name属性 |
通过元素name属性查找,在Android中一般可以用text代替 |
findElementByAndroidUIAutomator | String Ui Automator查找代码 |
使用UI Automator来查找元素 |
findElementByClassName | String 类名,要写全路径:android.weight.TextView |
通过元素类名查找 |
findElementById | String 元素id,android:id/title |
通过元素id查找 |
findElementByAccessibilityId | String 元素的contentDescription属性 |
通过contentDescription属性查找 |
findEelementByXPath | String 元素的XPath |
通过XPath查找 |
findElementByCssSelector | WebView专用 | |
findElementByLinkText | WebView专用 | |
findElementByPartialLinkText | WebView专用 |
注: 每一个方法对应着一个
findElementsBy***
方法,后者返回一个Element集合List,表示一个符合查找规则的一个Element集合
findElementByName
在Android中,没有合适的方法可以找到控件的Name属性,但是大多数情况下可以用控件的text代替name。
findElementByAndroidUIAutomator
使用UI Automator查找控件:
WebElement el = driver.findElementByAndroidUIAutomator("new UiSelector().text(\"Add note\")");
更多关于UI Automator定位元素的方法,详见:UI Automator定位元素
findElementByClassName
WebElement el = driver.findElementByClassName("android.weight.TextView");
findElementById
WebElement el = driver.findElementById("android:id/title");
如果目标设备的API Level低于18则UIAutomatorViewer不能获得对应的Resource ID,只有等于大于18的时候才能使用。
findElementByAccessibilityId
WebElement el = driver.findElementByAccessibilityId("menu_add_note_description");
Accessibility ID在Android上面就等同于contentDescription
findEelementByXPath
WebElement el = driver.findElementByXPath("//android.widget.TextView[contains(@text,'Add note')]");
xPath是一种路径,在uiautomatorviewer中可以查看当前页面的元素层级,XPath就是用来描述这种层级关系的一种路径表达方式,比如,下图中的例子,想找列表中第二个note1,则可以这样写:
WebElement el = driver.findElementByXPath("//android.widget.LinearLayout[1]/android.widget.FrameLayout/android.widget.ListView/android.widget.TextView[contains(@index,0)]");
findElementByCssSelector
这个方法是针对WebView容器下面的控件定位的,因为现在针对的是Native App暂时还没有用到,所以先标记下,今后需要的时候加上去。
findElementByLinkText
这个方法是针对WebView容器下面的控件定位的,因为现在针对的是Native App暂时还没有用到,所以先标记下,今后需要的时候加上去。
findElementByPartialLinkText
这个方法是针对WebView容器下面的控件定位的,因为现在针对的是Native App暂时还没有用到,所以先标记下,今后需要的时候加上去。
UI Automater定位元素
UI Automator中主要通过UISelector类查找元素
通过文本信息定位
返回值 | 方法名 | 说明 | 用法 |
---|---|---|---|
UiSelector | text(String text) | 根据“控件text属性的内容”构造出UiSelector对象 | 例如,一个控件text的值是“发现”,UiSelector s = new UiSelector().text("发现"); |
UiSelector | textContains(String text) | 根据“控件text属性包含的内容”构造出UiSelector对象 | 同上例子:UiSelector s = new UiSelector().textContains("现"); |
UiSelector | textMatches(String regex) | 根据“控件text属性正则表达式的内容”构造出UiSelector对象 | 正则表达式语法参考网上资料 |
UiSelector | textStartsWith(String text) | 根据“控件text属性开始的内容”构造出UiSelector对象 | 同上例子:UiSelector s = new UiSelector().textStartsWith("发"); |
通过description定位
返回值 | 方法名 | 说明 |
---|---|---|
UiSelector | description(String desc) | 根据“控件content-desc属性的内容”构造出UiSelector对象 |
UiSelector | descriptionContains(String desc) | 包含** |
UiSelector | descriptionMatches(String regex) | 正则 |
UiSelector | descriptionStartsWith(String desc) | 以**开始 |
通过ResourceId定位
返回值 | 方法名 | 说明 |
---|---|---|
UiSelector | resourceId(String id) | 根据资源id获取对象,例如:UiSelector s = new UiSelector().resourceId("com.tencent.mm:id/b8m") |
UiSelector | resourceIdMatches(String regex) | 根据资源id的正则表达式获取对象 |
通过类名定位
返回值 | 方法名 | 说明 |
---|---|---|
UiSelector | className(String className) | 根据控件类名找到对象 |
UiSelector | classNameMatches(String regex) | 根据类名的正则表达式获取对象 |
UiSelector | instance(int instance) | 找到一个拥有相同属性的对象集合中的对象,例如:UiSelector s = new UiSelector().className("android.widget.TextView").instance(1);可以找到页面层级中第二个类名为TextView的元素 |
UiSelector | index(int index) | 用法和上面的instance差不多,谷歌的原文说这个方法是unreliable的,推荐使用instance方法 |
通过层级关系
返回值 | 方法名 | 说明 |
---|---|---|
UiSelector | fromParent(UiSelector s) | 获取同一个父控件的其他子控件,即获取兄弟控件 |
UiSelector | getFromParent(UiSelector s) | 获取同一个父控件的其他子控件,即获取兄弟控件 |
UiSelector | childSelector(UiSelector s) | 获取子控件 |
UiSelector | getChild(UiSelector s) | 获取子控件 |
使用JUnit组织测试用例
使用JUnit来组织Appium测试用例,可以添加@Before
、@Test
或者@After
等注解来使测试代码的运行变得更加灵活。
比如在开始测试之前,需要配置Capability和连接Appium服务器,而且一般需要安装应用或者打开应用的某个Activity。这时候可把这些操作放到一个方法中,并且方法添加@Before
注解,则在运行的时候,@Before
方法会先于@Test
方法执行。
同理,测试结束后需要关闭session,回收资源等等,可以把这些操作放到一个@After
方法中,@After
方法在所有@Test
方法结束之后执行。
显式等待和隐式等待
有时候,由于网络或者其他原因,页面跳转之后,某些元素没有立即显示出来,此时查找元素会失败。这种情况就需要引入显式等待
和隐式等待
线程等待
直接使用Thread.sleep();
隐式等待
driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
全局等待30秒,不管元素是否已经加载
显式等待
WebDriverWait
显示等待,显示等待时间可以通过 WebDriverWait
和 util
来决定,比如这个 timeOut
是60,如果该元素60s以内出现就不在等待。
WebDriverWait wait = new WebDriverWait(driver, 60);
WebElement e= wait.until(new ExpectedCondition() {
@Override
public WebElement apply(WebDriver d) {
return d.findElement(By.id("q"));
}
})