文章目录

背景说明

因为项目需要做android web自动化,遂调研方案Selendroid和Appium

Selendroid

说明

相关代码 https://github.com/fqcheng220/selendroiddemo

Selendroid由四部分组成

1.selendroid server运行在pc端
2.selendroid client测试脚本运行在pc端
3.待测试apk运行在android设备端
4.待测试apk对应的intrumentation apk运行在android设备端

如果是做android native自动化的话,开发人员需要完成三部分

开发人员需要完成1、2、3,
编写native待测试apk,输出apk文件,以apk文件路径作为参数启动selendroid server
并编写selendroid client测试脚本运行

其中4.待测试apk对应的intrumentation apk运行在android设备端(这是selendroid server通过模板instrumentation apk和运行selendroid server时指定的待测试apk,生成待测试apk对应的intrumentation apk)

如果是做android web自动化的话,开发人员需要完成两部分

开发人员需要完成1、2,启动selendroid server并编写selendroid client测试脚本运行

其中3.待测试apk运行在android设备端(这是selendroid server自带的一个android driver apk,只负责webview测试,不需要开发人员自己编写)

4也不需要,同native自动化一致

1.selendroid server

首先在pc端启动selendroid server,版本选择的是0.17.0,很久没有维护过了,这也是最后一个版本

环境准备

1.android sdk,并配置好环境变量(adb)
2.jdk

错误1:android.bat运行报错Invalid or unsupported command “list avds”

启动selendroid-standalone-0.17.0-with-dependencies.jar报错

C:\Users\Administrator\Downloads>java -jar selendroid-standalone-0.17.0-with-dependencies.jar

报错内容如下

六月 07, 2023 9:42:57 上午 io.selendroid.standalone.server.model.DeviceStore addDevice
信息: Adding: HardwareDevice [serial=8DF6R16826005374, model=null, targetVersion=null, apiTargetType=google]
六月 07, 2023 9:42:57 上午 io.selendroid.standalone.android.impl.DefaultDeviceManager initializeAdbConnection
信息: my devices: null
六月 07, 2023 9:42:57 上午 io.selendroid.standalone.io.ShellCommand exec
信息: Executing shell command: D:\MyWork\Android\Tools\AndroidSDK\tools\android.bat list avds
六月 07, 2023 9:42:57 上午 io.selendroid.standalone.io.ShellCommand exec
严重: Error executing command: D:\MyWork\Android\Tools\AndroidSDK\tools\android.bat list avds
org.apache.commons.exec.ExecuteException: Process exited with an error: 1 (Exit value: 1)
        at org.apache.commons.exec.DefaultExecutor.executeInternal(DefaultExecutor.java:377)
        at org.apache.commons.exec.DefaultExecutor.execute(DefaultExecutor.java:160)
        at org.apache.commons.exec.DefaultExecutor.execute(DefaultExecutor.java:147)
        at io.selendroid.standalone.io.ShellCommand.exec(ShellCommand.java:49)
        at io.selendroid.standalone.android.impl.DefaultAndroidEmulator.listAvailableAvds(DefaultAndroidEmulator.java:152)
        at io.selendroid.standalone.server.model.DeviceStore.initAndroidDevices(DeviceStore.java:124)
        at io.selendroid.standalone.server.model.SelendroidStandaloneDriver.initAndroidDevices(SelendroidStandaloneDriver.java:185)
        at io.selendroid.standalone.server.model.SelendroidStandaloneDriver.<init>(SelendroidStandaloneDriver.java:95)
        at io.selendroid.standalone.server.SelendroidStandaloneServer.initializeSelendroidServer(SelendroidStandaloneServer.java:63)
        at io.selendroid.standalone.server.SelendroidStandaloneServer.<init>(SelendroidStandaloneServer.java:52)
        at io.selendroid.standalone.SelendroidLauncher.launchServer(SelendroidLauncher.java:65)
        at io.selendroid.standalone.SelendroidLauncher.main(SelendroidLauncher.java:117)

六月 07, 2023 9:42:57 上午 io.selendroid.standalone.SelendroidLauncher launchServer
严重: Error building server: io.selendroid.standalone.exceptions.ShellCommandException: Error executing shell command: D:\MyWork\Android\Tools\AndroidSDK\tools\android.bat list avds
Exception in thread "main" java.lang.RuntimeException: io.selendroid.standalone.exceptions.AndroidDeviceException: io.selendroid.standalone.exceptions.ShellCommandException: Error executing shell command: D:\MyWork\Android\Tools\AndroidSDK\tools\android.bat list avds
        at com.google.common.base.Throwables.propagate(Throwables.java:160)
        at io.selendroid.standalone.SelendroidLauncher.launchServer(SelendroidLauncher.java:75)
        at io.selendroid.standalone.SelendroidLauncher.main(SelendroidLauncher.java:117)
Caused by: io.selendroid.standalone.exceptions.AndroidDeviceException: io.selendroid.standalone.exceptions.ShellCommandException: Error executing shell command: D:\MyWork\Android\Tools\AndroidSDK\tools\android.bat list avds
        at io.selendroid.standalone.android.impl.DefaultAndroidEmulator.listAvailableAvds(DefaultAndroidEmulator.java:154)
        at io.selendroid.standalone.server.model.DeviceStore.initAndroidDevices(DeviceStore.java:124)
        at io.selendroid.standalone.server.model.SelendroidStandaloneDriver.initAndroidDevices(SelendroidStandaloneDriver.java:185)
        at io.selendroid.standalone.server.model.SelendroidStandaloneDriver.<init>(SelendroidStandaloneDriver.java:95)
        at io.selendroid.standalone.server.SelendroidStandaloneServer.initializeSelendroidServer(SelendroidStandaloneServer.java:63)
        at io.selendroid.standalone.server.SelendroidStandaloneServer.<init>(SelendroidStandaloneServer.java:52)
        at io.selendroid.standalone.SelendroidLauncher.launchServer(SelendroidLauncher.java:65)
        ... 1 more
Caused by: io.selendroid.standalone.exceptions.ShellCommandException: Error executing shell command: D:\MyWork\Android\Tools\AndroidSDK\tools\android.bat list avds
        at io.selendroid.standalone.io.ShellCommand.exec(ShellCommand.java:56)
        at io.selendroid.standalone.android.impl.DefaultAndroidEmulator.listAvailableAvds(DefaultAndroidEmulator.java:152)
        ... 7 more
Caused by: io.selendroid.standalone.exceptions.ShellCommandException: **************************************************************************
The "android" command is deprecated.
For manual SDK, AVD, and project management, please use Android Studio.
For command-line tools, use tools\bin\sdkmanager.bat
and tools\bin\avdmanager.bat
**************************************************************************

Invalid or unsupported command "list avds"

Supported commands are:
android list target
android list avd
android list device
android create avd
android move avd
android delete avd
android list sdk
android update sdk

        ... 9 more

因为新版本android sdk中的android.bat不支持参数list avds

解决方案:

直接修改android sdk下的tools/android.bat文件(记得修改之前备份),先跑通再说

在这里插入图片描述

错误2:java.lang.NullPointerException Error while creating new session

严重: Error while creating new session
java.lang.NullPointerException
	at io.selendroid.standalone.android.impl.DefaultHardwareDevice.unlockScreen(DefaultHardwareDevice.java:111)
	at io.selendroid.standalone.server.model.SelendroidStandaloneDriver.createNewTestSession(SelendroidStandaloneDriver.java:234)
	at io.selendroid.standalone.server.model.SelendroidStandaloneDriver.createNewTestSession(SelendroidStandaloneDriver.java:214)
	at io.selendroid.standalone.server.handler.CreateSessionHandler.handleRequest(CreateSessionHandler.java:40)
	at io.selendroid.standalone.server.BaseSelendroidStandaloneHandler.handle(BaseSelendroidStandaloneHandler.java:45)
	at io.selendroid.standalone.server.SelendroidServlet.handleRequest(SelendroidServlet.java:131)
	at io.selendroid.server.common.BaseServlet.handleHttpRequest(BaseServlet.java:67)
	at io.selendroid.server.common.http.ServerHandler.channelRead(ServerHandler.java:53)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:333)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:319)
	at io.netty.handler.traffic.AbstractTrafficShapingHandler.channelRead(AbstractTrafficShapingHandler.java:223)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:333)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:319)
	at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:333)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:319)
	at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:163)
	at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:148)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:333)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:319)
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:787)
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:125)
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:511)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:468)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:382)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:354)
	at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:116)
	at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:137)
	at java.lang.Thread.run(Unknown Source)
public String createNewTestSession(JSONObject caps, Integer retries) {
        AndroidDevice device = null;
        AndroidApp app = null;
        Exception lastException = null;

        while(retries >= 0) {
            try {
                SelendroidCapabilities desiredCapabilities = this.getSelendroidCapabilities(caps);
                String desiredAut = desiredCapabilities.getDefaultApp(this.appsStore.keySet());
                app = this.getAndroidApp(desiredCapabilities, desiredAut);
                log.info("'" + desiredAut + "' will be used as app under test.");
                device = this.deviceStore.findAndroidDevice(desiredCapabilities);
                if (device instanceof AndroidEmulator) {
                    this.startAndroidEmulator(desiredCapabilities, (AndroidEmulator)device);
                } else {
                    device.unlockScreen();//出错点!!!!!!!!!!!!!!!!!!!
                }

                boolean appInstalledOnDevice = device.isInstalled(app) || app instanceof InstalledAndroidApp;
                if (appInstalledOnDevice && !this.serverConfiguration.isForceReinstall()) {
                    log.info("the app under test is already installed.");
                } else {
                    device.install(app);
                }

                if (!this.serverConfiguration.isNoClearData()) {
                    device.clearUserData(app);
                }

                int port = this.getNextSelendroidServerPort();
                boolean serverInstalled = device.isInstalled("io.selendroid." + app.getBasePackage());
                if (serverInstalled && !this.serverConfiguration.isForceReinstall()) {
                    log.info("Not creating and installing selendroid-server because it is already installed for this app under test.");
                } else {
                    try {
                        device.install(this.createSelendroidServerApk(app));
                    } catch (AndroidSdkException var18) {
                        throw new SessionNotCreatedException("Could not install selendroid-server on the device", var18);
                    }
                }

                List<String> preSessionAdbCommands = desiredCapabilities.getPreSessionAdbCommands();
                this.runPreSessionCommands(device, preSessionAdbCommands);
                String extensionFile = desiredCapabilities.getSelendroidExtensions();
                this.pushExtensionsToDevice(device, extensionFile);
                device.setLoggingEnabled(this.serverConfiguration.isDeviceLog());
                this.eventListener.onBeforeDeviceServerStart();
                device.startSelendroid(app, port, desiredCapabilities);
                this.waitForServerStart(device);
                this.eventListener.onAfterDeviceServerStart();

                try {
                    Thread.sleep(500L);
                } catch (InterruptedException var17) {
                    Thread.currentThread().interrupt();
                }

                RemoteWebDriver driver = new RemoteWebDriver(new URL("http://localhost:" + port + "/wd/hub"), desiredCapabilities);
                String sessionId = driver.getSessionId().toString();
                SelendroidCapabilities requiredCapabilities = new SelendroidCapabilities(driver.getCapabilities().asMap());
                ActiveSession session = new ActiveSession(sessionId, requiredCapabilities, app, device, port, this);
                this.sessions.put(sessionId, session);
                if ("android".equals(desiredCapabilities.getAut())) {
                    this.switchToWebView(driver);
                }

                return sessionId;
            } catch (Exception var19) {
                lastException = var19;
                log.log(Level.SEVERE, "Error occurred while starting Selendroid session", var19);
                retries = retries - 1;
                if (device != null) {
                    this.deviceStore.release(device, app);
                    device = null;
                }
            }
        }

        if (lastException instanceof RuntimeException) {
            throw (RuntimeException)lastException;
        } else {
            throw new SessionNotCreatedException("Error starting Selendroid session", lastException);
        }
    }

可以看到查找适配的device之后执行unlock解锁方法

DefaultHardwareDevice.java

public void unlockScreen() throws AndroidDeviceException {
        String output = this.runAdbCommand("shell dumpsys power");
        String value;
        if (Integer.parseInt(this.targetPlatform.getApi()) >= 20) {//出错点!!!!!!!!!!!!!!!!!!!
            value = extractValue("Display Power: state=(.*?)$", output);
            if (value.equals("OFF")) {
                this.inputKeyevent(26);
            }
        } else {
            value = extractValue("mScreenOn=(.*?)$", output);
            if (value.equals("false")) {
                this.inputKeyevent(26);
            }
        }

    }

这里的成员变量targetPlatform为空导致的

解决方案:运行时反射修改

核心代码

/**
         * 以下代码是修改devicestore中所有设备信息,增加targetPlatform成员变量为DeviceTargetPlatform.ANDROID23
         * 以跳过检查,不加的话,默认4444端口的server在接受客户端创建session请求时会报空指针
         *
         * 1.基于selendroid-standalone:0.17.0源码去修改实现
         * 2.使用selendroid-standalone:0.17.0,反射修改行为状态(此处使用)
         */
        SelendroidStandaloneDriver driver = selendroidServer.getServer().getDriver();
        DeviceStore deviceStore = ReflectUitls.getDeviceStore(driver);
        if (deviceStore != null) {
            List<AndroidDevice> list = deviceStore.getDevices();
            if (list != null) {
                for (AndroidDevice device : list) {
                    if (device != null) {
                        if (device instanceof DefaultHardwareDevice) {
                            ReflectUitls.setTargetPlatform((DefaultHardwareDevice) device, DeviceTargetPlatform.ANDROID23);
                        } else if (device instanceof DefaultAndroidEmulator) {
                            ReflectUitls.setTargetPlatform((DefaultAndroidEmulator) device, DeviceTargetPlatform.ANDROID23);
                        }
                    }

                }
            }
        }

这里使用DeviceTargetPlatform.ANDROID23作为默认的TargetPlatform是因为0.17.0的版本只是支持到android23,后面也没有更高android版本支持了,先跑通再说

错误3.高版本android系统的安装问题:

安装测试包的重新签名包会报错“INSTALL_PARSE_FAILED_NO_CERTIFICATES”

解决方案:

暂时换成低版本android系统(6.0或者以下)

错误4.instrumentation运行问题:(未解决!)

selendroid instrumentation包运行会提示与target包签名不一致导致无法运行instrumentation包(java.lang.SecurityException: Permission Denial)

Caused by: io.selendroid.standalone.exceptions.ShellCommandException: java.lang.SecurityException: Permission Denial: starting instrumentation ComponentInfo{io.selendroid.com.fqcheng220.android.selendroiddemoapk/io.selendroid.server.ServerInstrumentation} from pid=5591, uid=5591 not allowed because package io.selendroid.com.fqcheng220.android.selendroiddemoapk does not have a signature matching the target com.fqcheng220.android.selendroiddemoapk
	at android.os.Parcel.readException(Parcel.java:1665)
	at android.os.Parcel.readException(Parcel.java:1618)
	at android.app.ActivityManagerProxy.startInstrumentation(ActivityManagerNative.java:4535)
	at com.android.commands.am.Am.runInstrument(Am.java:889)
	at com.android.commands.am.Am.onRun(Am.java:400)
	at com.android.internal.os.BaseCommand.run(BaseCommand.java:51)
	at com.android.commands.am.Am.main(Am.java:121)
	at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method)
	at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:262)
解决方案:(待解决!)

2.selendroid client测试脚本

selendroid-client库版本也是0.17.0,间接引用的org.seleniumhq.selenium:selenium-remote-driver库版本2.48.0

环境准备

jdk

web

WebDriver driver = new RemoteWebDriver(DesiredCapabilities.android());
//        driver.get("http://www.baidu.com");
            driver.get("http://baidu.com");


            if (driver == null)
                return;
            WebElement inputField = driver.findElement(By.id("index-kw"));
//        Assert.assertEquals("true", inputField.getAttribute("enabled"));
            inputField.sendKeys("Selendroid");
//        Assert.assertEquals("Selendroid", inputField.getText());
            WebElement btnConfirm = driver.findElement(By.id("index-bn"));
            btnConfirm.click();

纯native

WebDriver driver = connect("io.selendroid.testapp:0.17.0");
            if (driver == null)
                return;
            WebElement inputField = driver.findElement(By.id("my_text_field"));
//        Assert.assertEquals("true", inputField.getAttribute("enabled"));
            inputField.sendKeys("Selendroid");
//        Assert.assertEquals("Selendroid", inputField.getText());
            driver.quit();
public WebDriver connect(final String aut) {
        SelendroidCapabilities capa = new SelendroidCapabilities(aut);
        try {
            WebDriver driver = new SelendroidDriver(capa);
            return driver;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

其中io.selendroid.testapp:0.17.0是selendroid server启动后返回的对应apk的aut信息(可以通过访问selendroid server端提供的http://localhost:4444/wd/hub/status接口获取)

3.待测试apk

需要在启动selendroid的时候指定待测试apk的访问路径,供后续selendroid server进行重新签名和通过adb install安装到android设备上

如果是web自动化测试,是不需要开发人员编写的

4.待测试apk对应的intrumentation apk

这是由selendroid server通过模板instrumentation apk和运行selendroid server时指定的待测试apk,生成待测试apk对应的intrumentation apk签名并通过通过adb install安装到android设备上,中间还会指定instrumentation运行参数启动一个运行在android设备上的服务(本身属于instrumentation apk的基本功能范畴),最后通过adb forward进行端口转发,统一由selendroid server作为网关路由跟selendroid client测试脚本交互

总结

selendroid在高版本android上经常有兼容性问题,6.0或者以下适配挺好,本身这个开源项目只是支持到android 23
另外还要考察下加固的android包是否都可以正常操作

Appium

官网 http://appium.io/docs/en/2.0/quickstart/

如果是做android web自动化的话,开发人员需要完成三部分

appium server
webview demo apk
appium client脚本

1.appium server

安装部分参考官网 http://appium.io/docs/en/2.0/quickstart/install/

这里用到版本是Appium v2.0.0-rc.2

环境准备

1.nodejs
2.android sdk,并配置好环境变量(adb)
3.jdk

安装

1.安装appium

npm i -g appium@next

安装appium driver,比如uiautomator2

appium driver install uiautomator2

2.android sdk下载和环境变量配置不赘述了

3.jdk配置也不赘述了

启动appium

appium

输出日志,提示已经安装了uiautomator2驱动,默认监听端口是4723

C:\Users\Administrator>appium
[Appium] Welcome to Appium v2.0.0-rc.2
[Appium] Attempting to load driver uiautomator2...
[debug] [Appium] Requiring driver at C:\Users\Administrator\.appium\node_modules\appium-uiautomator2-driver
[Appium] Appium REST http interface listener started on http://0.0.0.0:4723
[Appium] You can provide the following URLS in your client code to connect to this server:
[Appium]        http://10.16.0.97:4723/
[Appium]        http://172.29.112.1:4723/
[Appium]        http://172.30.66.58:4723/
[Appium]        http://192.168.217.2:4723/
[Appium]        http://192.168.187.2:4723/
[Appium]        http://192.168.188.2:4723/
[Appium]        http://192.168.51.2:4723/
[Appium]        http://169.254.103.3:4723/
[Appium]        http://192.168.32.2:4723/
[Appium]        http://192.168.182.2:4723/
[Appium]        http://192.168.78.2:4723/
[Appium]        http://192.168.219.2:4723/
[Appium]        http://192.168.30.2:4723/
[Appium]        http://192.168.5.1:4723/
[Appium]        http://192.168.205.1:4723/
[Appium]        http://127.0.0.1:4723/
[Appium] Available drivers:
[Appium]   - uiautomator2@2.26.1 (automationName 'UiAutomator2')
[Appium] No plugins have been installed. Use the "appium plugin" command to install the one(s) you want to use

2.webview demo apk

可以直接指定android手机上的chrome浏览器,这也要求手机上安装了chrome浏览器,所以不如自己写一个webview apk,但是**有一个比较麻烦的点是如果url不同,需要重新打webview apk包,不灵活(看看有没有其他解决方案) **

不需要重新打包的解决方案:(2023.06.20)

通过设置UIAutomator2 server可以识别且android平台特有的capability(intent相关)创建appium session,
来达到实现一个webview apk包可以指定不同url参数启动的目的

DesiredCapabilities options = new DesiredCapabilities();
        options.setCapability("automationName", "UIAutomator2");
        options.setCapability("platformName", "Android");
//        options.setCapability("browserName", "Chrome");
        options.setCapability("app", PATH_APK_WEBVIEW_DEMO);
//                .setApp(PATH_APK_WEBVIEW_DEMO)
        /**
         * 以下两句代码可以实现appium server通过adb 启动目标activity的时候定制intent启动参数
         * 比如定制intent action和intent data,只要目标activity实现了解析对应intent action和intent data逻辑即可,通常是根据action和data动态获取weburl去load
         * 而且使用了这种方式后,后续appium脚本操作dom树也是不会有变化了(不应该调用方法oldDemoBaiduOper,因为oldDemoBaiduOper中url是有变化的从而dom树也是不一样的 ,而应该调用方法newDemoBaiduOper)
         *
         * 这里的appium:optionalIntentArguments使用方式获取方法:
         * 1.先通过查询官网 https://github.com/appium/appium-uiautomator2-driver#capabilities(因为本项目已经指定了appium server的automationName使用UIAutomator2)
         * 2.再去查看appium server运行日志获取到(有adb shell am start xxx日志,
         * 类似D:\MyWork\Android\Tools\AndroidSDK\platform-tools\adb.exe -P 5037 -s 8DF6R16826005374 shell am start -W -n com.fqcheng220.android.selendroidwebdemoapk/com.fqcheng220.android.selendroidwebdemoapk.MainActivity -S -a android.intent.action.VIEW -c android.intent.category.LAUNCHER -f 0x10200000 -d http://www.baidu.com)

         * ************************开始************************
         */
        options.setCapability("appium:intentAction", "android.intent.action.VIEW");
        options.setCapability("appium:optionalIntentArguments", "-d http://www.baidu.com");
        /**
         * ************************结束************************
         */
        options.setCapability("chromedriverExecutable",PATH_CHROME_DRIVER_WIN_2_28);

        AndroidDriver driver = null;
        try {
            driver = new AndroidDriver(
                    // The default URL in Appium 1 is http://127.0.0.1:4723/wd/hub
                    new URL("http://127.0.0.1:4723"), options
            );
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }

说明

完整webview demo apk源码
https://github.com/fqcheng220/selendroidwebdemoapk

3.appium client脚本

这里使用java版appium client库

https://github.com/appium/java-client

这里用的com.github.appium:java-client版本为8.5.1

说明

完整的appium client脚本源码
https://github.com/fqcheng220/appiumdemo

环境准备

1.jdk
2.刚刚的webview demo apk
3.chromedriver

这里特别提一下chromedriver,虽然是给appium server用的,但是需要在appium client这里设置访问路径
chromedriver版本

各个版本chromedriver官网下载地址 https://chromedriver.storage.googleapis.com/index.html

webview内核版本与chromedriver版本对照关系

webview内核版本与chromedriver版本对照关系 https://raw.githubusercontent.com/appium/appium-chromedriver/master/config/mapping.json

代码

这里例举一个百度首页自动输入appium关键字搜索的案例

关键代码段

public void start() {
//        UiAutomator2Options options = new UiAutomator2Options()
                .setUdid("8DF6R16826005374")
//                .setApp(PATH_APK_WEBVIEW_DEMO)
//                .setChromedriverExecutable(PATH_CHROME_DRIVER_WIN_89_0);

        DesiredCapabilities options = new DesiredCapabilities();
        options.setCapability("automationName", "UIAutomator2");
        options.setCapability("platformName", "Android");
//        options.setCapability("browserName", "Chrome");//这里可以不需要
        options.setCapability("app", PATH_APK_WEBVIEW_DEMO);//这里是webview demo apk的访问路径
//                .setApp(PATH_APK_WEBVIEW_DEMO)
        options.setCapability("chromedriverExecutable",PATH_CHROME_DRIVER_WIN_83_0);//这里是chrome driver的访问路径,重要!!!
        AndroidDriver driver = null;
        try {
            driver = new AndroidDriver(
                    // The default URL in Appium 1 is http://127.0.0.1:4723/wd/hub
                    new URL("http://127.0.0.1:4723"), options
            );
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        if (driver != null) {
            try {
                WebElement el = driver.findElement(By.className("android.webkit.WebView"));
                System.out.println("" + driver.getContext());
                for (String ctx : driver.getContextHandles()) {
                    System.out.println("-------" + ctx);
                }
                /**
                 * 非常重要,只有经过这步才能操作webview里的dom元素
                 * 但是存在一个问题:
                 * 就是以这种方式启动的webdemoapk加载的url跟直接启动webdemoapk加载的url是不一致的,前者增加了很多查询参数导致返回的hmtl内容有差异,dom树节点都发生了变化
                 * 原因未知,可能是
                 *
                 * 所以需要在加载新的url下返回的html中查找需要的节点
                 *
                 * 跟selendroid情况类似
                 */
                driver.context("WEBVIEW_com.fqcheng220.android.selendroidwebdemoapk");
                /**
                 * 以下尝试切换webview context的调用方法实现是错误的
                 */
//                driver.switchTo().window("WEBVIEW_com.fqcheng220.android.selendroidwebdemoapk");

                WebElement inputField = driver.findElement(By.id("word"));
                inputField.sendKeys("Appium");
                try {
                    Thread.sleep(5*1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                /**
                 * android webview中加载的html内容如下
                 *  <input type="submit" value="百度一下" class="bn" />
                 *
                 *  起初调用WebElement btnConfirm = driver.findElement(By.className(".bn"));
                 *  始终查找不到dom节点!
                 *
                 *  原因:在调用这个方法之前切换过webview上下文
                 *  而在切换到webview上下文之前使用的native上下文上有做过class查找
                 *  WebElement el = driver.findElement(By.className("android.webkit.WebView"));
                 *
                 *  跟踪源码
                 *  ElementLocation.java
                 *
                 *  函数findElement
                 *  public WebElement findElement(RemoteWebDriver driver, SearchContext context, BiFunction<String, Object, CommandPayload> createPayload, By locator) {
                 *         Require.nonNull("WebDriver", driver);
                 *         Require.nonNull("Context for finding elements", context);
                 *         Require.nonNull("Method for creating remote requests", createPayload);
                 *         Require.nonNull("Locator", locator);
                 *         ElementLocation.ElementFinder mechanism = (ElementLocation.ElementFinder)this.finders.get(locator.getClass());
                 *         if (mechanism != null) {
                 *             return mechanism.findElement(driver, context, createPayload, locator);
                 *         } else {
                 *             WebElement element;
                 *             if (locator instanceof Remotable) {
                 *                 try {
                 *                     element = ElementLocation.ElementFinder.REMOTE.findElement(driver, context, createPayload, locator);
                 *                     this.finders.put(locator.getClass(), ElementLocation.ElementFinder.REMOTE);
                 *                     return element;
                 *                 } catch (NoSuchElementException var8) {
                 *                     this.finders.put(locator.getClass(), ElementLocation.ElementFinder.REMOTE);
                 *                     throw var8;
                 *                 } catch (InvalidArgumentException var9) {
                 *                     ;
                 *                 }
                 *             }
                 *
                 *             try {
                 *                 element = ElementLocation.ElementFinder.CONTEXT.findElement(driver, context, createPayload, locator);
                 *                 this.finders.put(locator.getClass(), ElementLocation.ElementFinder.CONTEXT);
                 *                 return element;
                 *             } catch (NoSuchElementException var7) {
                 *                 this.finders.put(locator.getClass(), ElementLocation.ElementFinder.CONTEXT);
                 *                 throw var7;
                 *             }
                 *         }
                 *     }
                 *
                 *     如果之前缓存过ByClassName类型对应的ElementLocation.ElementFinder实例到finders,则下次如果使用ByClassName查找dom节点的话则直接使用之前缓存的ElementLocation.ElementFinder实例
                 *     也就是REMOTE实例,但是webview中dom树节点是没办法通过REMOTE以ByClassName形式访问的,必须让REMOTE以ByCssSelector形式访问
                 *     因此有两种解决方案:
                 *     方案1:
                 *     切换webview context上下文之前不要用native context根据ByClassName查找原生控件
                 *
                 *     方案2:
                 *     webview中的dom节点查找使用By.cssSelector(".bn")形式,其中bn是节点的class,bn之前有一个点(非常重要!!!)
                 *     本案例使用的就是方案2
                 */
                WebElement btnConfirm = driver.findElement(By.cssSelector(".bn"));
                btnConfirm.click();
                while (true);
            } finally {
                driver.quit();
            }
        }
    }

这里代码有变化(2023.06.20),主要是加上了android intent启动参数的配置还有操作dom树节点的变更,具体可以看 完整的appium client脚本源码

<repositories>
        <repository>
            <id>jitpack.io</id>
            <url>https://jitpack.io</url>
        </repository>
    </repositories>
    <dependencies>
        <dependency>
            <groupId>com.github.appium</groupId>
            <artifactId>java-client</artifactId>
            <version>8.5.1</version>
        </dependency>
    </dependencies>

编译运行脚本

梳理流程
1.配置webview demo apk的访问路径和chrome driver访问路径,连接appium server(默认端口4723),执行完之后web view demo apk就通过adb am start在android设备启动起来
1.1 配置代码如下
DesiredCapabilities options = new DesiredCapabilities();
        options.setCapability("automationName", "UIAutomator2");
        options.setCapability("platformName", "Android");
//        options.setCapability("browserName", "Chrome");//这里可以不需要
        options.setCapability("app", PATH_APK_WEBVIEW_DEMO);//这里是webview demo apk的访问路径
//                .setApp(PATH_APK_WEBVIEW_DEMO)
        options.setCapability("chromedriverExecutable",PATH_CHROME_DRIVER_WIN_83_0);//这里是chrome driver的访问路径,重要!!!


这里代码有变化(2023.06.20),需要加上android intent启动参数的配置

/**
         * 以下两句代码可以实现appium server通过adb 启动目标activity的时候定制intent启动参数
         * 比如定制intent action和intent data,只要目标activity实现了解析对应intent action和intent data逻辑即可,通常是根据action和data动态获取weburl去load
         * 而且使用了这种方式后,后续appium脚本操作dom树也是不会有变化了(不应该调用方法oldDemoBaiduOper,因为oldDemoBaiduOper中url是有变化的从而dom树也是不一样的 ,而应该调用方法newDemoBaiduOper)
         *
         * 这里的appium:optionalIntentArguments使用方式获取方法:
         * 1.先通过查询官网 https://github.com/appium/appium-uiautomator2-driver#capabilities(因为本项目已经指定了appium server的automationName使用UIAutomator2)
         * 2.再去查看appium server运行日志获取到(有adb shell am start xxx日志,
         * 类似D:\MyWork\Android\Tools\AndroidSDK\platform-tools\adb.exe -P 5037 -s 8DF6R16826005374 shell am start -W -n com.fqcheng220.android.selendroidwebdemoapk/com.fqcheng220.android.selendroidwebdemoapk.MainActivity -S -a android.intent.action.VIEW -c android.intent.category.LAUNCHER -f 0x10200000 -d http://www.baidu.com)

         * ************************开始************************
         */
        options.setCapability("appium:intentAction", "android.intent.action.VIEW");
        options.setCapability("appium:optionalIntentArguments", "-d http://www.baidu.com");
        /**
         * ************************结束************************
         */
1.2 连接appium server代码如下
try {
            driver = new AndroidDriver(
                    // The default URL in Appium 1 is http://127.0.0.1:4723/wd/hub
                    new URL("http://127.0.0.1:4723"), options
            );
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
2.因为是默认使用driver使用的native context,如果想操作webview中的dom树,需要手动切换到webview context

切换context代码如下

/**
                 * 非常重要,只有经过这步才能操作webview里的dom元素
                 * 但是存在一个问题:
                 * 就是以这种方式启动的webdemoapk加载的url跟直接启动webdemoapk加载的url是不一致的,前者增加了很多查询参数导致返回的hmtl内容有差异,dom树节点都发生了变化
                 * 原因未知,可能是
                 *
                 * 所以需要在加载新的url下返回的html中查找需要的节点
                 *
                 * 跟selendroid情况类似
                 */
                driver.context("WEBVIEW_com.fqcheng220.android.selendroidwebdemoapk");
                /**
                 * 以下尝试切换webview context的调用方法实现是错误的
                 */
//                driver.switchTo().window("WEBVIEW_com.fqcheng220.android.selendroidwebdemoapk");
3.操作dom树节点

通过com.github.appium:java-client:8.5.1间接引入的org.seleniumhq.selenium:selenium-remote-driver库版本为4.10.0

通过class查找dom节点

这里如果使用class方式去查询dom节点的话,需要class名称前加点,调用方法是RemoteWebDriver#findElement(By locator),方法入参是By.cssSelector(“.class名称”)类型,如

WebElement btnConfirm = driver.findElement(By.cssSelector(".bn"));
通过id查找dom节点
WebElement inputField = driver.findElement(By.id("word"));

原理

WebDriver(重要!!!)

如果是做android native自动化的话,开发人员也是需要完成三部分,只不过webview apk换成了android native apk

android控件定位方式

工具之uiautomatorview

这是android sdk自带工具,跟android studio上的layout inspector不一样,后者只能对debug签名的包进行inspect,前者是所有release包(即使加固了)都可以(只要没有动画之类的页面布局即可)
也不同于早期的hirearchyviewer也是要求debug签名包的

在android sdk目录下的位置
{android sdk}/tools/bin/uiautomatorviewer.bat

原理(重要!!!)

可能使用的android系统
/system/bin/uiautomator脚本完成

android7.0中 uiautomator脚本内容如下

#
# Copyright (C) 2012 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Script to start "uiautomator" on the device
#
# The script does a couple of things:
# * Use an alternative dalvik cache when running as non-root. Jar file needs
#   to be dexopt'd to run in Dalvik. For plain jar files, this is done at first
#   use. shell user does not have write permission to default system Dalvik
#   cache so we redirect to an alternative cache
# * special processing for subcommand 'runtest':
#    * '--nohup' allows process continue to run even if parent process that
#      started it has already terminated. We parse for this parameter and set
#      signal trap. This is useful for testing with USB disconnected
#    * all jar files that the test classes resides in, or dependent on are
#      provided on command line and exported to CLASSPATH environment variable
#      before starting the Java code. This offloads the task of class loading
#      and resolving of cross jar class dependency to Dalvik
#    * all other subcommand or options are directly passed into Java code for
#      further parsing

export run_base=/data/local/tmp
export base=/system

# if not running as root, trick dalvik into using an alternative dex cache
if [ ${USER_ID} -ne 0 ]; then
  tmp_cache=${run_base}/dalvik-cache

  if [ ! -d ${tmp_cache} ]; then
    mkdir -p ${tmp_cache}
  fi

  export ANDROID_DATA=${run_base}
fi

# take first parameter as the command
cmd=${1}

if [ -z "${1}" ]; then
  cmd="help"
fi

# strip the command parameter
if [ -n "${1}" ]; then
  shift
fi

CLASSPATH=/system/framework/android.test.runner.jar:${base}/framework/uiautomator.jar

# eventually args will be what get passed down to Java code
args=
# we also pass the list of jar files, so we can extract class names for tests
# if they are not explicitly specified
jars=

# special case pre-processing for 'runtest' command
if [ "${cmd}" == "runtest" ]; then
  # Print deprecation warning
  echo "Warning: This version of UI Automator is deprecated. New tests should be written using"
  echo "UI Automator 2.0 which is available as part of the Android Testing Support Library."
  echo "See https://developer.android.com/training/testing/ui-testing/uiautomator-testing.html"
  echo "for more details."
  # first parse the jar paths
  while [ true ]; do
    if [ -z "${1}" ] && [ -z "${jars}" ]; then
      echo "Error: more parameters expected for runtest; please see usage for details"
      cmd="help"
      break
    fi
    if [ -z "${1}" ]; then
      break
    fi
    jar=${1}
    if [ "${1:0:1}" = "-" ]; then
      # we are done with jars, starting with parameters now
      break
    fi
    # if relative path, append the default path prefix
    if [ "${1:0:1}" != "/" ]; then
      jar=${run_base}/${1}
    fi
    # about to add the file to class path, check if it's valid
    if [ ! -f ${jar} ]; then
      echo "Error: ${jar} does not exist"
      # force to print help message
      cmd="help"
      break
    fi
    jars=${jars}:${jar}
    # done processing current arg, moving on
    shift
  done
  # look for --nohup: if found, consume it and trap SIG_HUP, otherwise just
  # append the arg to args
  while [ -n "${1}" ]; do
    if [ "${1}" = "--nohup" ]; then
      trap "" HUP
      shift
    else
      args="${args} ${1}"
      shift
    fi
  done
else
  # if cmd is not 'runtest', just take the rest of the args
  args=${@}
fi

args="${cmd} ${args}"
if [ -n "${jars}" ]; then
   args="${args} -e jars ${jars}"
fi

CLASSPATH=${CLASSPATH}:${jars}
export CLASSPATH
exec app_process ${base}/bin com.android.commands.uiautomator.Launcher ${args}

问题
1.遇到windows平台下 uiautomatorview.bat启动失败

可能是java版本不对,java8支持,java11不支持,具体需要看android sdk的版本(主要是uiautomatorview.bat脚本中运行的jar版本适配的java版本)

2.有些页面无法通过device screenshot(uiautomator dump)截图获取xml布局

可能是页面中有很多动态控件

工具之appium desktop

类似uiautomatorview

总结

无论使用何种方式,只要不是存在动态控件的(比如动画之类的),都可以抓取到页面xml布局

具体xml中元素是通过id、name、content-desc还是xpath需要根据实际情况决定,一般优先是id、name、content-desc,都没有的情况下使用xpath

android控件操作方式

appium client
appium client的原理
Instrumentation
UiAutomator(重要!!!)

总结

如果在android设备上进行web测试,appium可以兼容android比较高的系统版本(比如android 11)
相比较selendroid,appium server跟selendroid server类似,
只需要额外安装一个webview demo apk(android设备上运行)和chrome driver程序即可(appium server需要使用运行)
但是比selendroid适配兼容性好多了,也更稳定,推荐!!!

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐