0%

Appium在Android中的使用

参考
helloworld: http://appium.io/docs/en/about-appium/getting-started/index.html
appium:https://github.com/appium
Android server: https://github.com/appium/appium-uiautomator2-server
uiautomator:https://developer.android.com/training/testing/ui-automator
uiautomator helloworld: https://github.com/android/testing-samples/blob/master/ui/uiautomator/BasicSample/app/src/androidTest/java/com/example/android/testing/uiautomator/BasicSample/ChangeTextBehaviorTest.java
webdriver标准:https://w3c.github.io/webdriver/
webdriverio 相关可以自由发挥

原理

appium是一款自动化工具,可以运行在Android,IOS,Windows平台,移动Web和混合应用程序。

整体流程:
未命名文件 (1).png

webdriver 标准:

未命名文件 (2).png

js实现自动化:

16294501451979.mp4```shell
2021-08-20T08:58:34.242Z INFO webdriver: COMMAND findElement(“-android uiautomator”, “new UiSelector().text(“同意并继续”);”)
2021-08-20T08:58:34.243Z INFO webdriver: [POST] http://127.0.0.1:4723/wd/hub/session/9a7e6db3-932c-4e69-a564-c6f68f943c02/element
2021-08-20T08:58:34.243Z INFO webdriver: DATA {
using: ‘-android uiautomator’,
value: ‘new UiSelector().text(“同意并继续”);’
}
2021-08-20T08:58:34.708Z INFO webdriver: RESULT {
‘element-6066-11e4-a52e-4f735466cecf’: ‘4f9013b3-41fd-4a80-ab06-33495186d444’,
ELEMENT: ‘4f9013b3-41fd-4a80-ab06-33495186d444’
}
2021-08-20T08:58:34.708Z INFO webdriver: COMMAND elementClick(“4f9013b3-41fd-4a80-ab06-33495186d444”)
2021-08-20T08:58:34.708Z INFO webdriver: [POST] http://127.0.0.1:4723/wd/hub/session/9a7e6db3-932c-4e69-a564-c6f68f943c02/element/4f9013b3-41fd-4a80-ab06-33495186d444/click
2021-08-20T08:58:34.834Z INFO webdriver: COMMAND findElement(“-android uiautomator”, “new UiSelector().text(“跳过”);”)
2021-08-20T08:58:34.834Z INFO webdriver: [POST] http://127.0.0.1:4723/wd/hub/session/9a7e6db3-932c-4e69-a564-c6f68f943c02/element
2021-08-20T08:58:34.834Z INFO webdriver: DATA {
using: ‘-android uiautomator’,
value: ‘new UiSelector().text(“跳过”);’
}
2021-08-20T08:58:36.237Z INFO webdriver: RESULT {
‘element-6066-11e4-a52e-4f735466cecf’: ‘4d7a7e62-b72c-4f83-91c4-eaed51b15295’,
ELEMENT: ‘4d7a7e62-b72c-4f83-91c4-eaed51b15295’
}
2021-08-20T08:58:36.237Z INFO webdriver: COMMAND elementClick(“4d7a7e62-b72c-4f83-91c4-eaed51b15295”)
2021-08-20T08:58:36.238Z INFO webdriver: [POST] http://127.0.0.1:4723/wd/hub/session/9a7e6db3-932c-4e69-a564-c6f68f943c02/element/4d7a7e62-b72c-4f83-91c4-eaed51b15295/click

1
2
3
4
5
6
7
8

---

# appium服务端

```json
npm install -g appium
appium

appium客户端

这里比较重要的是webdriverio的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// javascript

const wdio = require("webdriverio");
const assert = require("assert");

const opts = {
path: '/wd/hub',
port: 4723,
capabilities: {
platformName: "Android",
platformVersion: "8",
deviceName: "Android Emulator",
app: "/path/to/the/downloaded/ApiDemos-debug.apk",
appPackage: "io.appium.android.apis",
appActivity: ".view.TextFields",
automationName: "UiAutomator2"
}
};

async function main () {
const client = await wdio.remote(opts);

const field = await client.$("android.widget.EditText");
await field.setValue("songpengfei");
const value = await field.getText();
assert.strictEqual(value,"songpengfei");

await client.deleteSession();
}

main();
1
node index.js
  • /path/to/the/downloaded/ApiDemos-debug.apk 需要替换为电脑本地apk路径,去这里下载apk:https://github.com/appium/appium/tree/master/sample-code/apps
  • Android Emulator 使用adb devices 看下设备名称
  • platformVersion 这里为8,替换手机android版本
  • 具体的指令是webdriverio提供的

总的来说 capabilities配置的是服务器apk的信息,后面会详细讲解android端
Screenshot_20210820-021500.pngScreenshot_20210820-021506.png


Android端

android端是比较有意思的,我们上面图中说Andorid中有一个服务端,那么它是怎么实现的呢?它其实利用了Android的自动化测试框架 uiautomator,利用test case 启动了一个服务,并且这里的服务是根据webdriver来实现的,首先来看一下Android项目的结构。

1 项目结构

image.png
androidTestE2eTest:因为Appium是通过Rustful接口来访问Android客户端的,所以Android服务端应该存在一套框架来解析请求数据,然后将数据转化为调用uiautomator模拟行为,androidTestE2eTest就是测试这一套框架的
androidTestServer:这里是Android服务端的入口
main:数据转uiautomator模拟行为这一套框架
test:也是测试框架的

这里有两个问题,

  1. 怎么去启动了一个Android端服务?
  2. uiautomator和无障碍权限有什么关系?

2 启动服务

2.1 服务端代码

2.1.1 自动化测试入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RunWith(AndroidJUnit4.class)
public class AppiumUiAutomator2Server {
private static ServerInstrumentation serverInstrumentation;

/**
* Starts the server on the device.
* !!! This class is the main entry point for UIA2 driver package.
* !!! Do not rename or move it unless you know what you are doing.
*/
@Test
public void startServer() {
if (serverInstrumentation == null) {
serverInstrumentation = ServerInstrumentation.getInstance();
Logger.info("[AppiumUiAutomator2Server]", " Starting Server");
try {
while (!serverInstrumentation.isServerStopped()) {
SystemClock.sleep(1000);
serverInstrumentation.startMjpegServer();
serverInstrumentation.startServer(); //启动服务
}
} catch (SessionRemovedException e) {
//Ignoring SessionRemovedException
}
}
}
}

2.1.2 启动服务代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class HttpServer {

private final List<IHttpServlet> handlers = new ArrayList<>();

public void addHandler(IHttpServlet handler) {
handlers.add(handler);
}

public void start() {
if (serverThread != null) {
throw new IllegalStateException("Server is already running");
}
serverThread = new Thread() {
@Override
public void run() {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.SO_REUSEADDR, true)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.childHandler(new ServerInitializer(handlers));
Log.d("端口号:", port + "");
Channel ch = bootstrap.bind(port).sync().channel();
ch.closeFuture().sync();
} catch (InterruptedException ignored) {
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
};
serverThread.start();
}
}

服务端使用了netty框架,这里比较重要的一个点是handlers

2.1.3 服务端接口,AppiumServlet(IHttpServlet)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class AppiumServlet implements IHttpServlet {


private void init() {
registerGetHandler(); //get请求
registerPostHandler(); //post请求
registerDeleteHandler(); //delete请求
}

private void registerDeleteHandler() {
register(deleteHandler, new DeleteSession("/wd/hub/session/:sessionId"));
}

private void registerPostHandler() {
register(postHandler, new NewSession("/wd/hub/session"));
register(postHandler, new FindElement("/wd/hub/session/:sessionId/element"));
register(postHandler, new FindElements("/wd/hub/session/:sessionId/elements"));
register(postHandler, new Click("/wd/hub/session/:sessionId/element/:id/click"));
register(postHandler, new Tap("/wd/hub/session/:sessionId/appium/tap"));
register(postHandler, new Clear("/wd/hub/session/:sessionId/element/:id/clear"));
register(postHandler, new SetOrientation("/wd/hub/session/:sessionId/orientation"));

...
}

private void registerGetHandler() {
...
register(getHandler, new GetBatteryInfo("/wd/hub/session/:sessionId/appium/device/battery_info"));
register(getHandler, new GetSettings("/wd/hub/session/:sessionId/appium/settings"));
register(getHandler, new GetDevicePixelRatio("/wd/hub/session/:sessionId/appium/device/pixel_ratio"));
register(getHandler, new FirstVisibleView("/wd/hub/session/:sessionId/appium/element/:id/first_visible"));
register(getHandler, new GetAlertText("/wd/hub/session/:sessionId/alert/text"));
register(getHandler, new GetDeviceInfo("/wd/hub/session/:sessionId/appium/device/info"));
}


@Override
public void handleHttpRequest(IHttpRequest request, IHttpResponse response) {
BaseRequestHandler handler = null;
if ("GET".equals(request.method())) {
handler = findMatcher(request, getHandler);
} else if ("POST".equals(request.method())) {
handler = findMatcher(request, postHandler);
} else if ("DELETE".equals(request.method())) {
handler = findMatcher(request, deleteHandler);
}
if (handler != null) {
handleRequest(request, response, handler);
}
}

}

2.2 请求接口

2.2.1 我们这个启动服务后,请求GetDeviceInfo这个接口

1
new GetDeviceInfo("/wd/hub/session/:sessionId/appium/device/info"));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class GetDeviceInfo extends SafeRequestHandler {

@Override
protected AppiumResponse safeHandle(IHttpRequest request) {
//返回结果
DeviceInfoModel result = new DeviceInfoModel();
final DeviceInfoHelper deviceInfoHelper = new DeviceInfoHelper(mInstrumentation.getTargetContext());
result.androidId = deviceInfoHelper.getAndroidId();
result.manufacturer = deviceInfoHelper.getManufacturer();
result.model = deviceInfoHelper.getModelName();
result.brand = deviceInfoHelper.getBrand();
result.apiVersion = deviceInfoHelper.getApiVersion();
result.platformVersion = deviceInfoHelper.getPlatformVersion();
result.carrierName = deviceInfoHelper.getCarrierName();
result.realDisplaySize = deviceInfoHelper.getRealDisplaySize();
result.displayDensity = deviceInfoHelper.getDisplayDensity();
result.networks = extractNetworkInfo(deviceInfoHelper);
result.locale = deviceInfoHelper.getLocale();
result.timeZone = deviceInfoHelper.getTimeZone();
result.bluetooth = extractBluetoothInfo(deviceInfoHelper);

return new AppiumResponse(getSessionId(request), result);
}
}

2.2.2这里有个:sessionId,我们需要改动代码让服务器session=songpengfei

1
2
3
4
5
6
7
8
9
10
11
12
public class Session {

private final String sessionId = "songpengfei";

Session(String sessionId, Map<String, Object> capabilities) {
// this.sessionId = sessionId;
}
}

public class AppiumUIA2Driver {
private Session session = new Session(null,new HashMap<String, Object>());
}

2.2.3顺便打印一下端口号,在HttpServer类中打印

1
2021-08-20 10:52:22.809 27880-27919/io.appium.uiautomator2.server D/端口号: 6790

2.2.4启动服务,这里有两种方式启动服务

  1. android studio启动

image.png
image.png

  1. 命令行启动
    1
    2
    3
    gradle clean assembleServerDebug assembleServerDebugAndroidTest
    // install 省略 ,如果你只想运行服务 assembleServerDebug就可以满足
    adb shell am instrument -w io.appium.uiautomator2.server.test/androidx.test.runner.AndroidJUnitRunner

2.2.5 浏览器访问

1
http://30.28.50.6:6790/wd/hub/session/songpengfei/appium/device/info

id地址:30.28.50.6(手机当前网络中查看),端口:6790(Logcat中输出的)
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{
"sessionId": "songpengfei",
"value": {
"androidId": "03ebd9bd8c0ab50e",
"apiVersion": "29",
"bluetooth": {
"state": "ON"
},
"brand": "Android",
"carrierName": "",
"displayDensity": 560,
"locale": "zh_CN_#Hans",
"manufacturer": "Google",
"model": "AOSP on coral",
"networks": [
{
"capabilities": {
"SSID": null,
"linkDownBandwidthKbps": 1048576,
"linkUpstreamBandwidthKbps": 1048576,
"networkCapabilities": "NET_CAPABILITY_NOT_METERED,NET_CAPABILITY_INTERNET,NET_CAPABILITY_NOT_RESTRICTED,NET_CAPABILITY_TRUSTED,NET_CAPABILITY_NOT_VPN,NET_CAPABILITY_VALIDATED,NET_CAPABILITY_NOT_ROAMING,NET_CAPABILITY_FOREGROUND,NET_CAPABILITY_NOT_CONGESTED,NET_CAPABILITY_NOT_SUSPENDED",
"signalStrength": -57,
"transportTypes": "TRANSPORT_WIFI"
},
"detailedState": "CONNECTED",
"extraInfo": null,
"isAvailable": true,
"isConnected": true,
"isFailover": false,
"isRoaming": false,
"state": "CONNECTED",
"subtype": 0,
"subtypeName": "",
"type": 1,
"typeName": "WIFI"
}
],
"platformVersion": "10",
"realDisplaySize": "1440x3040",
"timeZone": "GMT"
}
}

大功告成,我们这里使用了浏览器去访问了手机服务端,appium中也是类似的行为,只不过api的定义是根据webdriver中的标准来的。和http协议标准一样,大家都去遵守这个标准,我们才能好好的完。

3 uiautomator和无障碍权限

下面是一段自动化测试的用例,在页面中找到OK按钮,然后点击它

1
2
3
4
5
6
7
UiObject okButton = device.findObject(new UiSelector()
.text("OK")
.className("android.widget.Button"));

if(okButton.exists() && okButton.isEnabled()) {
okButton.click();
}

UiObject click()

1
2
3
4
5
6
7
8
9
10
public boolean click() throws UiObjectNotFoundException {
Tracer.trace();
AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(mUiSelector.toString());
}
Rect rect = getVisibleBounds(node);
return getInteractionController().clickAndSync(rect.centerX(), rect.centerY(),
mConfig.getActionAcknowledgmentTimeout());
}

如果你了解无障碍权限的话,你会知道AccessibilityNodeInfo无障碍中页面的元素节点,并且UiAutomation中也提供了onAccessibilityEvent方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public final class UiAutomation {
public static interface OnAccessibilityEventListener {
public void onAccessibilityEvent(AccessibilityEvent event);
}
public void setOnAccessibilityEventListener(OnAccessibilityEventListener listener) {
synchronized (mLock) {
mOnAccessibilityEventListener = listener;
}
}
}
public abstract class AccessibilityService extends Service {
public abstract void onAccessibilityEvent(AccessibilityEvent event);
}

webdriverio标准

webdriverio的api真的很蛋疼,建议最好是结合Android中的api实现来看,否则你根本无法理解webdriverio中的api含义
eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//模拟打开高德,然后点击  同意并继续和进入地图
async function main() {
const client = await wdio.remote(opts);

const agreeBtn = await client.findElement('-android uiautomator','new UiSelector().text("同意并继续");')
await client.elementClick(agreeBtn.ELEMENT);

const interMap = await client.findElement('-android uiautomator','new UiSelector().text("进入地图");')
await client.elementClick(interMap.ELEMENT);

await client.deleteSession();
}

main();

Android 端上代码

  1. -android uiautomator是什么,它是寻找element的一种策略,支持5种模式

    1
    2
    3
    4
    5
    6
    7
    8
    //android 端上代码
    public enum ElementsLookupStrategy {
    BY_ID("id"),
    BY_XPATH("xpath"),
    BY_ACCESSIBILITY_ID("accessibility id"),
    BY_CLASS("class name"),
    BY_UIAUTOMATOR("-android uiautomator");
    }
  2. **new UiSelector().text(“同意并继续”); 是什么?

**

UiSelector和UiScrollable是uiautomator中寻找element的一种方式
“new UiSelector().text(“同意并继续”);”,最终会通过字符串分割和反射的方式解析并初始化为Android端上的UiSelector

1
2
3
4
5
6
7
8
9
10
11
public class ElementLocationHelpers {

public static List<UiSelector> toSelectors(String uiaExpression) throws UiSelectorSyntaxException,
UiObjectNotFoundException {
List<UiSelector> selectors = new UiAutomatorParser().parse(uiaExpression);
if (selectors.isEmpty()) {
throw new UiSelectorSyntaxException(uiaExpression);
}
return selectors;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class UiAutomatorParser {

private static final String STATEMENT_DELIMITER = ";";
private final List<UiSelector> selectors = new ArrayList<>();
private String text;

public List<UiSelector> parse(String textToParse) throws UiSelectorSyntaxException,
UiObjectNotFoundException {
selectors.clear();
if (textToParse.isEmpty()) {
throw new UiSelectorSyntaxException(textToParse, "Tried to parse an empty string. " +
"Expected to see a string consisting of text to be interpreted as " +
"UiAutomator java code.");
}
text = textToParse.trim();
removeTailingSemicolon(); // 移除最后一个;

while (text.length() > 0) {
consumeStatement(); 构造
consumeSemicolon();
}

return selectors;
}

private void consumeStatement() throws UiSelectorSyntaxException, UiObjectNotFoundException {
text = text.trim();
String statement;
int index = 0;
boolean isInsideStringLiteral = false;
while (index < text.length()) {
final char currentChar = text.charAt(index);

if (currentChar == '"') {
/* Skip escaped quotes */
isInsideStringLiteral = !(isInsideStringLiteral && index > 0
&& text.charAt(index - 1) != '\\');
}

if (STATEMENT_DELIMITER.equals(String.valueOf(text.charAt(index)))
&& !isInsideStringLiteral) {
break;
}
index++;
}

statement = text.substring(0, index).trim();
UiScrollableParser uiScrollableParser = createUiScrollableParser(statement);
if (uiScrollableParser.isUiScrollable()) {
Logger.debug("Parsing scrollable: " + statement);
selectors.add(uiScrollableParser.parse()); // uiScrollable
} else {
Logger.debug("Parsing selector: " + statement);
selectors.add(createUiSelectorParser(statement).parse()); // uiselector
}

text = text.substring(index);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class UiSelectorParser extends UiExpressionParser<UiSelector, UiSelector> {



public UiSelector parse() throws UiSelectorSyntaxException, UiObjectNotFoundException {
resetCurrentIndex();
consumeConstructor(); // new UiSelector
while (hasMoreDataToParse()) { //调用 UiSelector 的方法
consumePeriod();
final Object result = consumeMethodCall();
if (!(result instanceof UiSelector)) {
throw new UiSelectorSyntaxException(expression.toString(),
String.format("Unsupported return value type:`%s`. " +
"Only methods with return type `UiSelector` are supported.",
result.getClass().getSimpleName()));
}
setTarget((UiSelector) result);
}
return getTarget();
}
}