参考 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和混合应用程序。
整体流程:
webdriver 标准:
js实现自动化:
```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 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();
总的来说 capabilities配置的是服务器apk的信息,后面会详细讲解android端
Android端 android端是比较有意思的,我们上面图中说Andorid中有一个服务端,那么它是怎么实现的呢?它其实利用了Android的自动化测试框架 uiautomator,利用test case 启动了一个服务,并且这里的服务是根据webdriver来实现的,首先来看一下Android项目的结构。
1 项目结构 androidTestE2eTest:因为Appium是通过Rustful接口来访问Android客户端的,所以Android服务端应该存在一套框架来解析请求数据,然后将数据转化为调用uiautomator模拟行为,androidTestE2eTest就是测试这一套框架的 androidTestServer:这里是Android服务端的入口 main:数据转uiautomator模拟行为这一套框架 test:也是测试框架的
这里有两个问题,
怎么去启动了一个Android端服务?
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; @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) { } } } }
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(); registerPostHandler(); registerDeleteHandler(); } 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) { } } 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启动服务,这里有两种方式启动服务
android studio启动
命令行启动1 2 3 gradle clean assembleServerDebug assembleServerDebugAndroidTest adb shell am instrument -w io.appium.uiautomator2.server.test/androidx.test.runner.AndroidJUnitRunner
2.2.5 浏览器访问
id地址:30.28.50.6(手机当前网络中查看),端口:6790(Logcat中输出的)
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();