参考资料
- appium desktop https://github.com/appium/appium-desktop/releases
- sdk与android studio https://developer.android.com/studio/?hl=zh-cn#downloads
- scrcpy https://github.com/Genymobile/scrcpy
- capability https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md
- appium官方提供的示例代码 https://github.com/appium/appium/blob/master/sample-code/
UI自动化价值
Appium安装
Android命令
adb logcat
02-21 21:03:28.811 1153 2077 I ActivityManager: START u0 {act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.xueqiu.android/.view.WelcomeActivityAlias bnds=[434,1257][645,1547]} from uid 10070
02-21 21:03:28.813 1153 2077 I ActivityManager: ActivityRecord info: ActivityInfo{15fee14 com.xueqiu.android.view.WelcomeActivityAlias}
02-21 21:03:28.815 723 758 D AwareLog: iawared: workingset_process_command subCmd = 354, uid=10192, pid=0, com.xueqiu.android
02-21 21:03:28.815 723 758 D AwareLog: iawared: workingset_send_preread_msg com.xueqiu.android
02-21 21:03:28.815 723 758 D AwareLog: iawared: workingsetStart com.xueqiu.android in monitor0
02-21 21:03:28.816 1881 2201 I hibernation: front pkg : com.xueqiu.android launcher: false
02-21 21:03:28.816 1881 2201 I hibernation: above launcher front pkgs: [com.xueqiu.android]
02-21 21:03:28.837 1153 2077 V WindowManager: addAppToken: AppWindowToken{157d780 token=Token{2722c03 ActivityRecord{a14c4b2 u0 com.xueqiu.android/.view.WelcomeActivityAlias t11782}}} controller={TaskWindowContainerController taskId=11782} at 2147483647
02-21 21:03:28.840 1153 1341 D HwPhoneWindowManager: addHwStartWindow set default on app : com.xueqiu.android
02-21 21:03:28.849 1153 2077 V ActivityManager: startProcess: name=com.xueqiu.android app=null knownToBeDead=true thread=null pid=-1
#手工打开某个界面
02-21 21:07:46.392 1153 1229 I ActivityManager: Displayed com.xueqiu.android/.stock.StockDetailActivity: +251ms
adb logcat | grep -i displayed
02-21 21:27:58.341 1153 1229 I ActivityManager: Displayed com.xueqiu.android/.view.WelcomeActivityAlias: +357ms
adb shell am start -W -S -n com.xueqiu.android/.view.WelcomeActivityAlias
Stopping: com.xueqiu.android
Starting: Intent { cmp=com.xueqiu.android/.view.WelcomeActivityAlias }
Status: ok
Activity: com.xueqiu.android/.view.WelcomeActivityAlias
ThisTime: 338
TotalTime: 338
WaitTime: 389
Complete
Appium Inspector
获取app的入口命令
- adb logcat | grep -i displayed
- adb shell dumpsys activity activities top
- apkanalyse
{
"platformName": "android",
"deviceName": "demo",
"appPackage": "com.xueqiu.android",
"appActivity": ".view.WelcomeActivityAlias"
}
page source
<?xml version="1.0" encoding="UTF-8"?>
<hierarchy rotation="0">
<android.widget.FrameLayout index="0" text="" class="android.widget.FrameLayout" package="com.xueqiu.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,85][1080,2159]" resource-id="" instance="0">
<android.widget.LinearLayout index="0" text="" class="android.widget.LinearLayout" package="com.xueqiu.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,85][1080,2159]" resource-id="" instance="0">
<android.widget.FrameLayout index="0" text="" class="android.widget.FrameLayout" package="com.xueqiu.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,85][1080,2159]" resource-id="android:id/content" instance="1">
<android.widget.RelativeLayout index="0" text="" class="android.widget.RelativeLayout" package="com.xueqiu.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,85][1080,2159]" resource-id="" instance="0">
<android.widget.ImageView NAF="true" index="0" text="" class="android.widget.ImageView" package="com.xueqiu.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[896,530][992,626]" resource-id="com.xueqiu.android:id/image_cancel" instance="0" />
<android.widget.LinearLayout index="1" text="" class="android.widget.LinearLayout" package="com.xueqiu.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[87,686][992,1642]" resource-id="com.xueqiu.android:id/container" instance="1">
<android.widget.TextView index="0" text="雪球需要获取下列权限
才可正常使用:" class="android.widget.TextView" package="com.xueqiu.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[147,776][932,934]" resource-id="" instance="0" />
<android.widget.LinearLayout index="1" text="" class="android.widget.LinearLayout" package="com.xueqiu.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[147,934][932,1342]" resource-id="com.xueqiu.android:id/permission_list" instance="2">
<android.widget.RelativeLayout index="0" text="" class="android.widget.RelativeLayout" package="com.xueqiu.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[147,994][932,1138]" resource-id="com.xueqiu.android:id/phone_permission" instance="1">
<android.widget.ImageView index="0" text="" class="android.widget.ImageView" package="com.xueqiu.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[147,994][291,1138]" resource-id="com.xueqiu.android:id/phone_permission_icon" instance="1" />
<android.widget.TextView index="1" text="手机/电话权限" class="android.widget.TextView" package="com.xueqiu.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[330,994][676,1067]" resource-id="com.xueqiu.android:id/phone_permission_name" instance="1" />
<android.widget.TextView index="2" text="校验IMEI&MSI码,防止账号被盗" class="android.widget.TextView" package="com.xueqiu.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[330,1079][932,1136]" resource-id="" instance="2" />
</android.widget.RelativeLayout>
<android.widget.RelativeLayout index="1" text="" class="android.widget.RelativeLayout" package="com.xueqiu.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[147,1198][932,1342]" resource-id="com.xueqiu.android:id/storage_permission" instance="2">
<android.widget.ImageView index="0" text="" class="android.widget.ImageView" package="com.xueqiu.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[147,1198][291,1342]" resource-id="com.xueqiu.android:id/storage_permission_icon" instance="2" />
<android.widget.TextView index="1" text="存储权限" class="android.widget.TextView" package="com.xueqiu.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[330,1198][546,1271]" resource-id="com.xueqiu.android:id/storage_permission_name" instance="3" />
<android.widget.TextView index="2" text="缓存图片和视频,降低流量消耗" class="android.widget.TextView" package="com.xueqiu.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[330,1283][918,1340]" resource-id="" instance="4" />
</android.widget.RelativeLayout>
</android.widget.LinearLayout>
<android.widget.TextView index="2" text="开启" class="android.widget.TextView" package="com.xueqiu.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[147,1432][932,1552]" resource-id="com.xueqiu.android:id/open" instance="5" />
</android.widget.LinearLayout>
</android.widget.RelativeLayout>
</android.widget.FrameLayout>
</android.widget.LinearLayout>
</android.widget.FrameLayout>
</hierarchy>
录制生成用例
python
# This sample code uses the Appium python client
# pip install Appium-Python-Client
# Then you can paste this into a file and simply run with Python
from appium import webdriver
caps = {}
caps["platformName"] = "android"
caps["deviceName"] = "demo"
caps["appPackage"] = "com.xueqiu.android"
caps["appActivity"] = ".view.WelcomeActivityAlias"
driver = webdriver.Remote("http://localhost:4723/wd/hub", caps)
el1 = driver.find_element_by_id("com.xueqiu.android:id/open")
el1.click()
el2 = driver.find_element_by_id("android:id/button1")
el2.click()
el3 = driver.find_element_by_id("com.xueqiu.android:id/tv_search")
el3.click()
el4 = driver.find_element_by_id("com.xueqiu.android:id/search_input_text")
el4.send_keys("pdd")
driver.quit()
java
import io.appium.java_client.MobileElement;
import io.appium.java_client.android.AndroidDriver;
import junit.framework.TestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.net.MalformedURLException;
import java.net.URL;
import org.openqa.selenium.remote.DesiredCapabilities;
public class SampleTest {
private AndroidDriver driver;
@Before
public void setUp() throws MalformedURLException {
DesiredCapabilities desiredCapabilities = new DesiredCapabilities();
desiredCapabilities.setCapability("platformName", "android");
desiredCapabilities.setCapability("deviceName", "demo");
desiredCapabilities.setCapability("appPackage", "com.xueqiu.android");
desiredCapabilities.setCapability("appActivity", ".view.WelcomeActivityAlias");
URL remoteUrl = new URL("http://localhost:4723/wd/hub");
driver = new AndroidDriver(remoteUrl, desiredCapabilities);
}
@Test
public void sampleTest() {
MobileElement el1 = (MobileElement) driver.findElementById("com.xueqiu.android:id/open");
el1.click();
MobileElement el2 = (MobileElement) driver.findElementById("android:id/button1");
el2.click();
MobileElement el3 = (MobileElement) driver.findElementById("com.xueqiu.android:id/tv_search");
el3.click();
MobileElement el4 = (MobileElement) driver.findElementById("com.xueqiu.android:id/search_input_text");
el4.sendKeys("pdd");
}
@After
public void tearDown() {
driver.quit();
}
}
uiautomatorviewer定位元素
不能与appium同时使用
定位技巧
常见的定位策略
- id (resource-id)
- accessibilityId (content-desc)
- xpath 全能支持
定位示例
- com.xueqiu.android:id/storage_permission_name
- storage_permission_name
- //*[@text=‘开启’]
- //*[contains(@text, ‘手机’)]
错误log定位
[info] e[35m[Appium]e[39m Welcome to Appium v1.10.0
[info] e[35m[Appium]e[39m Appium REST http interface listener started on 0.0.0.0:4723[info] e[35m[HTTP]e[39m e[37m-->e[39m e[37mPOSTe[39m e[37m/wd/hub/sessione[39m
[info] e[35m[HTTP]e[39m e[90m{"desiredCapabilities":{"newCommandTimeout":0,"connectHardwareKeyboard":true,"browserName":"firefox","version":"","javascriptEnabled":true,"platform":"ANY"}}e[39m
[debug] e[35m[MJSONWP]e[39m Calling AppiumDriver.createSession() with args: [{"newCommandTimeout":0,"connectHardwareKeyboard":true,"browserName":"firefox","version":"","javascriptEnabled":true,"platform":"ANY"},null,null]
[debug] e[35m[BaseDriver]e[39m Event 'newSessionRequested' logged at 1550757878960 (22:04:38 GMT+0800 (CST))
[debug] e[35m[BaseDriver]e[39m Event 'newSessionStarted' logged at 1550757878960 (22:04:38 GMT+0800 (CST))
[debug] e[35m[MJSONWP]e[39m Encountered internal error running command: Error: You must include a platformName capability
[debug] e[35m[MJSONWP]e[39m at AppiumDriver.getDriverForCaps (/Applications/Appium.app/Contents/Resources/app/node_modules/appium/lib/appium.js:174:13)
[debug] e[35m[MJSONWP]e[39m at AppiumDriver.createSession (/Applications/Appium.app/Contents/Resources/app/node_modules/appium/lib/appium.js:259:32)
[debug] e[35m[MJSONWP]e[39m at AppiumDriver.executeCommand (/Applications/Appium.app/Contents/Resources/app/node_modules/appium-base-driver/lib/basedriver/driver.js:301:19)
[debug] e[35m[MJSONWP]e[39m at AppiumDriver.executeCommand (/Applications/Appium.app/Contents/Resources/app/node_modules/appium/lib/appium.js:411:26)
[debug] e[35m[MJSONWP]e[39m at asyncHandler (/Applications/Appium.app/Contents/Resources/app/node_modules/appium-base-driver/lib/protocol/protocol.js:352:34)
[debug] e[35m[MJSONWP]e[39m at app.(anonymous function) (/Applications/Appium.app/Contents/Resources/app/node_modules/appium-base-driver/lib/protocol/protocol.js:489:15)
[debug] e[35m[MJSONWP]e[39m at Layer.handle [as handle_request] (/Applications/Appium.app/Contents/Resources/app/node_modules/express/lib/router/layer.js:95:5)
[debug] e[35m[MJSONWP]e[39m at next (/Applications/Appium.app/Contents/Resources/app/node_modules/express/lib/router/route.js:137:13)
[debug] e[35m[MJSONWP]e[39m at Route.dispatch (/Applications/Appium.app/Contents/Resources/app/node_modules/express/lib/router/route.js:112:3)
[debug] e[35m[MJSONWP]e[39m at Layer.handle [as handle_request] (/Applications/Appium.app/Contents/Resources/app/node_modules/express/lib/router/layer.js:95:5)
[debug] e[35m[MJSONWP]e[39m at /Applications/Appium.app/Contents/Resources/app/node_modules/express/lib/router/index.js:281:22
[debug] e[35m[MJSONWP]e[39m at Function.process_params (/Applications/Appium.app/Contents/Resources/app/node_modules/express/lib/router/index.js:335:12)
[debug] e[35m[MJSONWP]e[39m at next (/Applications/Appium.app/Contents/Resources/app/node_modules/express/lib/router/index.js:275:10)
[debug] e[35m[MJSONWP]e[39m at logger (/Applications/Appium.app/Contents/Resources/app/node_modules/morgan/index.js:144:5)
[debug] e[35m[MJSONWP]e[39m at Layer.handle [as handle_request] (/Applications/Appium.app/Contents/Resources/app/node_modules/express/lib/router/layer.js:95:5)
[debug] e[35m[MJSONWP]e[39m at trim_prefix (/Applications/Appium.app/Contents/Resources/app/node_modules/express/lib/router/index.js:317:13)
[debug] e[35m[MJSONWP]e[39m at /Applications/Appium.app/Contents/Resources/app/node_modules/express/lib/router/index.js:284:7
[debug] e[35m[MJSONWP]e[39m at Function.process_params (/Applications/Appium.app/Contents/Resources/app/node_modules/express/lib/router/index.js:335:12)
[debug] e[35m[MJSONWP]e[39m at next (/Applications/Appium.app/Contents/Resources/app/node_modules/express/lib/router/index.js:275:10)
[debug] e[35m[MJSONWP]e[39m at /Applications/Appium.app/Contents/Resources/app/node_modules/body-parser/lib/read.js:130:5
[debug] e[35m[MJSONWP]e[39m at invokeCallback (/Applications/Appium.app/Contents/Resources/app/node_modules/raw-body/index.js:224:16)
[debug] e[35m[MJSONWP]e[39m at done (/Applications/Appium.app/Contents/Resources/app/node_modules/raw-body/index.js:213:7)
[debug] e[35m[MJSONWP]e[39m at IncomingMessage.onEnd (/Applications/Appium.app/Contents/Resources/app/node_modules/raw-body/index.js:273:7)
[debug] e[35m[MJSONWP]e[39m at IncomingMessage.emit (events.js:182:13)
[debug] e[35m[MJSONWP]e[39m at endReadableNT (_stream_readable.js:1090:12)
[debug] e[35m[MJSONWP]e[39m at process._tickCallback (internal/process/next_tick.js:63:19)
[info] e[35m[HTTP]e[39m e[37m<-- POST /wd/hub/session e[39me[31m500e[39m e[90m12 ms - 179e[39m
[info] e[35m[HTTP]e[39m e[90me[39m
[info] e[35m[HTTP]e[39m e[37m-->e[39m e[37mDELETEe[39m e[37m/wd/hub/sessione[39m
[info] e[35m[HTTP]e[39m e[90m{}e[39m
[debug] e[35m[HTTP]e[39m No route found. Setting content type to 'text/plain'
[info] e[35m[HTTP]e[39m e[37m<-- DELETE /wd/hub/session e[39me[33m404e[39m e[90m5 ms - 57e[39m
[info] e[35m[HTTP]e[39m e[90me[39m
自定义控件识别
- 基本定位
- 父控件定位+百分比定位
- 图像识别 https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/image-comparison.md
- ocr 图形文字识别
作业
- 搜索股票,并点击拼多多的股票
- 添加自选股,添加阿里巴巴股票,验证阿里巴巴已经在自主股中
把代码贴到回复里,如果能够以pytest的形式编写最佳