当前位置:   article > 正文

Android自动化测试框架实现_安卓自动化测试框架

安卓自动化测试框架

背景介绍

        最近打算梳理一下不同产品领域的自动化测试实现方案,如:Android终端、Web、服务端、智能硬件等,就先从Android终端产品开始梳理吧。本文主要介绍UI自动化测试的实现,因为这类测试解决方案比较通用,Android系统层、内核层的自动化测试解决方案可能要根据公司的具体业务来定了。

开源工具

        目前已经有比较不错的一些开源工具,如:Appnium、Python-UiAutomator,功能基本差不多,实现原理也是一样的,这里不做过多描述,有兴趣的同学可以查阅相关资料学习。

自研工具详述

        既然已经有开源的工具了,为什么还要自研?

        1、根据公司业务需要,满足定制化需求

        2、结合公司自动化框架,打造更加归一化、集成化的平台

        3、降低维护成本、提升易用性

一、实现原理概述

        我们先讲一下PC怎么跟Android终端通信,进而实现对它的控制,方式有两种:有线方式和无线方式。

有线方式:PC跟终端通过USB线连接,这种方式其实是通过adb作为中间媒介来实现通信的,如下图所示:

上图提到了Adb Client、Adb Server、Adb Daemon三个关键要素,下面简单讲一下三者的作用:

Adb Client:这部分就是需要我们自研开发的socket客户端

Adb Server:其实就是通过adb start-server命令在PC上启动的一个socket服务

Adb Daemon:是在终端上后台运行的一个守护进程,开机自启动,而且即使被杀掉,系统也会重新启动该进程,主要作用是跟Adb Server进行连接通信

注意:Adb Client与Adb Server通信遵循adb协议,具体协议内容可以百度查一下,相关资料很多

        通过以上内容大家应该清楚了PC怎么连接终端并且跟终端进行通信,那么重点来了,想要自研UI自动化测试工具就必须开发一个应用软件实现对uiautomator的封装,并在终端上作为Server运行起来,那么Adb Client跟我们开发的终端应用软件是如何进行通信的呢?如下图所示:

         本质上就是通过adb forward进行端口转发,关于这部分的内容不在这里做重点介绍,大家可以去查询adb协议。

无线方式:PC跟终端都连接到了同一个网络,通过网络进行通信,如下图所示:

         可以看出,相比有线方式,无线方式更加简单直接,PC作为客户端、应用软件作为Server端,二者通过建立socket通信来实现数据交互,从而实现PC对终端的控制。

 总结:不管哪种通信方式,要实现自研工具,有两块要进行开发(PC侧的客户端和终端侧的应用软件)。

二、Adb Client代码示例

Adb Client代码示例:
 

  1. # -*- coding:utf-8 -*-
  2. import os
  3. import subprocess
  4. import socket
  5. import whichcraft
  6. from collections import namedtuple
  7. from components.device.android.adb.errors import AdbError
  8. _OKAY = "OKAY"
  9. _FAIL = "FAIL"
  10. _DENT = "DENT"  # Directory Entity
  11. _DONE = "DONE"
  12. ForwardItem = namedtuple("ForwardItem", ["serial", "local", "remote"])
  13. def where_adb():
  14.     adb_path = whichcraft.which('adb')
  15.     if adb_path is None:
  16.         raise EnvironmentError("Can't find adb,please install adb first.")
  17.     return adb_path
  18. class _AdbStreamConnect(object):
  19.     """
  20.             连接adb服务
  21.             即通过adb start-server在PC侧ANDROID_ADB_SERVER_PORT端口起的服务
  22.             具体可以发送哪些命令需要查看adb协议
  23.     """
  24.     def __init__(self, host=None, port=None):
  25.         self.__host = host
  26.         self.__port = port
  27.         self.__conn = None
  28.         self._connect()
  29.     def _create_socket(self):
  30.         adb_host = self.__host or os.environ.get('ANDROID_ADB_SERVER_HOST', '127.0.0.1')
  31.         adb_port = self.__port or int(os.environ.get('ANDROID_ADB_SERVER_PORT', 7305))
  32.         s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  33.         s.settimeout(5)
  34.         try:
  35.             s.connect((adb_host, adb_port))
  36.             return s
  37.         except:
  38.             s.close()
  39.             raise
  40.     @property
  41.     def conn(self):
  42.         return self.__conn
  43.     def _connect(self):
  44.         try:
  45.             self.__conn = self._create_socket()
  46.         except Exception as e:
  47.             subprocess.Popen('adb kill-server')
  48.             subprocess.Popen('adb start-server')
  49.             self.__conn = self._create_socket()
  50.     def close(self):
  51.         self.conn.close()
  52.     def __enter__(self):
  53.         return self
  54.     def __exit__(self, exc_type, exc_val, exc_tb):
  55.         self.close()
  56.     def send(self, cmd):
  57.         # cmd: str表示期望cmd是字符串类型
  58.         if not isinstance(cmd, str):
  59.             cmd = str(cmd)
  60.         self.conn.send("{:04x}{}".format(len(cmd), cmd).encode("utf-8"))
  61.     def read(self, n):
  62.         # -> str表示接口返回值为字符串
  63.         return self.conn.recv(n).decode()
  64.     def read_string(self):
  65.         size = int(self.read(4), 16)
  66.         return self.read(size)
  67.     def read_until_close(self):
  68.         content = ""
  69.         while True:
  70.             chunk = self.read(4096)
  71.             if not chunk:
  72.                 break
  73.             content += chunk
  74.         return content
  75.     def check_okay(self):
  76.         # 前四位是状态码
  77.         data = self.read(4)
  78.         if data == _FAIL:
  79.             raise AdbError(self.read_string())
  80.         elif data == _OKAY:
  81.             return
  82.         raise AdbError("Unknown data: %s" % data)
  83. class AdbClient(object):
  84.     def __init__(self, host=None, port=None):
  85.         self.__host = host
  86.         self.__port = port
  87.     def server_version(self):
  88.         """
  89.         @summary:获取adb版本号
  90.         @return :版本号1.0.41中的41
  91.         """
  92.         with self._connect() as c:
  93.             c.send("host:version")
  94.             c.check_okay()
  95.             return int(c.read_string(), 16)
  96.     def _connect(self):
  97.         return _AdbStreamConnect(self.__host, self.__port)
  98.     def forward(self, serial, local, remote, norebind=False):
  99.         """
  100.         @summary:给adb服务端发送host-serial:<sn>:forward:tcp:<pc_port>;tcp:<phone_port>进行端口转发
  101.         @param serial:手机sn号,sn为None时按默认连接一部手机处理
  102.         @param local:PC侧socket客户端端口
  103.         @param remote:手机侧socket服务端端口
  104.         @param norebind:fail if already forwarded when set to true
  105.         @attention :PC跟手机通过USB方式通信
  106.         """
  107.         with self._connect() as c:
  108.             cmds = ["host", "forward"]
  109.             if serial:
  110.                 cmds = ["host-serial", serial, "forward"]
  111.             if norebind:
  112.                 cmds.append("norebind")
  113.             cmds.append("tcp:%s;tcp:%s" % (local, remote))
  114.             print(cmds)
  115.             c.send(":".join(cmds))
  116.             c.check_okay()
  117.     def forward_list(self, serial=None):
  118.         """
  119.         @summary:查看端口转发是否成功
  120.         @param serial:手机sn号
  121.         @attention :PC跟手机通过USB方式通信
  122.         """
  123.         with self._connect() as c:
  124.             list_cmd = "host:list-forward"
  125.             if serial:
  126.                 list_cmd = "host-serial:{}:list-forward".format(serial)
  127.             c.send(list_cmd)
  128.             c.check_okay()
  129.             content = c.read_string()
  130.             for line in content.splitlines():
  131.                 parts = line.split()
  132.                 if len(parts) != 3:
  133.                     continue
  134.                 if serial and parts[0] != serial:
  135.                     continue
  136.                 yield ForwardItem(*parts)
  137.     def shell(self, serial, command):
  138.         """
  139.         @summary:执行shell命令
  140.         @param serial:手机sn号
  141.         @param command:要执行的命令
  142.         @attention :只能执行adb shell命令
  143.         """
  144.         with self._connect() as c:
  145.             c.send("host:transport:" + serial)
  146.             c.check_okay()
  147.             c.send("shell:" + command)
  148.             c.check_okay()
  149.             return c.read_until_close()
  150.     def device_list(self):
  151.         """
  152.         @summary:获取手机列表
  153.         @attention :
  154.         """
  155.         device_list = []
  156.         with self._connect() as c:
  157.             c.send("host:devices")
  158.             c.check_okay()
  159.             output = c.read_string()
  160.             for line in output.splitlines():
  161.                 parts = line.strip().split("\t")
  162.                 if len(parts) != 2:
  163.                     continue
  164.                 if parts[1] == 'device':
  165.                     device_list.append(parts[0])
  166.         return device_list
  167.     def must_one_device(self, serial):
  168.         device_list = self.device_list()
  169.         if len(device_list) == 0:
  170.             raise RuntimeError(
  171.                 "Can't find any android device/emulator"
  172.             )
  173.         elif serial is None and len(device_list) > 1:
  174.             raise RuntimeError(
  175.                 "more than one device/emulator, please specify the serial number"
  176.             )

uiautomator.py代码实现:

  1. # -*- coding: utf-8 -*-
  2. # @Time : 2023/2/5 20:24
  3. # @Author : 十年
  4. # @Site : https://gitee.com/chshao/aiplotest
  5. # @CSDN : https://blog.csdn.net/m0_37576542?type=blog
  6. # @File : uiautomator.py
  7. # @Description : 对这个模块文件的总体描述
  8. import re
  9. import time
  10. import json
  11. import socket
  12. from typing import Union
  13. from aitest.buildin.AiDecorator import singleton
  14. from aitest.components.android.adb import AdbClient
  15. TAG_UI = "UiAutomator"
  16. TAG_WIFI = "WifiManager"
  17. TAG_STUB = "Stub"
  18. PORT10086 = 10086
  19. PORT12306 = 12306
  20. ip_pattern = re.compile('\d+.\d+.\d+.\d+')
  21. @singleton
  22. class UiClient(object):
  23. gSocket = None
  24. def connect(self, addr: Union[None, str, tuple]):
  25. """
  26. @summary:PC连接手机
  27. @param addr:手机sn号或者(ip,port)
  28. @attention :
  29. """
  30. if isinstance(addr, tuple):
  31. self._connect_wifi(addr[0], addr[1])
  32. self._connect_usb(addr)
  33. def _connect_usb(self, serial):
  34. adb_client = AdbClient()
  35. adb_client.must_one_device(serial)
  36. self._check_and_forward(adb_client, serial)
  37. def _check_and_forward(self, adb_client: AdbClient, serial=None):
  38. forward_list = adb_client.forward_list(serial)
  39. for forwardItem in forward_list:
  40. if forwardItem.serial == serial:
  41. port = forwardItem.local.split(':')[-1]
  42. self._connect_wifi('127.0.0.1', port)
  43. # 通过adb在pc上创建一个server监听10086端口,并将10086端口收到的数据转发给手机server的12306端口
  44. adb_client.forward(serial, PORT10086, PORT12306)
  45. # 创建一个socket客户端连接10086端口
  46. self._connect_wifi('127.0.0.1', PORT10086)
  47. def _connect_wifi(self, host, port):
  48. self.gSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  49. self.gSocket.connect((host, port))
  50. self.gSocket.settimeout(2)
  51. def send_cmd(self, cmd):
  52. cmd_bytes = json.dumps(cmd).encode('utf-8')
  53. cmd_len = len(cmd_bytes).to_bytes(4, "little")
  54. self.gSocket.send(cmd_len + cmd_bytes)
  55. def recv_ack(self, timeout=10):
  56. endTime = time.time() + timeout
  57. while time.time() < endTime:
  58. # 取前4位包头,为数据内容长度
  59. dataLenBytes = self.gSocket.recv(4)
  60. if dataLenBytes != b"":
  61. dataLen = int.from_bytes(dataLenBytes, "little")
  62. # 取出数据内容并返回
  63. return self.gSocket.recvData(dataLen)
  64. return b""
  65. def close(self):
  66. if self.gSocket:
  67. self.gSocket.close()
  68. class Device(object):
  69. def __init__(self, addr=None):
  70. """
  71. @param addr: None/手机sn/IP
  72. """
  73. self.ui = UiClient()
  74. if addr is not None:
  75. if re.match(ip_pattern, addr):
  76. addr = (addr, PORT12306)
  77. self.ui.connect(addr)
  78. def command(self, clsName, method, args=None):
  79. cmd = {"class": clsName,
  80. "method": method,
  81. "args": args,
  82. "requestId": "request_" + str(round(time.time(), 3))}
  83. return cmd
  84. def executeCmd(self, clsName, method, args=None):
  85. self.ui.send_cmd(self.command(clsName, method, args))
  86. ack = self.ui.recv_ack()
  87. if ack:
  88. return json.loads(ack.decode('utf-8')).get("result")
  89. return ""
  90. def __checkResult(self, ack, expect):
  91. if ack == expect:
  92. return True
  93. return False
  94. def click(self, x, y):
  95. args = [{"k": "int", "v": x}, {"k": "int", "v": y}]
  96. ack = self.executeCmd(TAG_UI, "click", args)
  97. return self.__checkResult(ack, 'true')
  98. def clickById(self, id):
  99. args = [{"k": "string", "v": id}]
  100. ack = self.executeCmd(TAG_UI, "clickById", args)
  101. return self.__checkResult(ack, 'true')
  102. def clickByText(self, text):
  103. args = [{"k": "string", "v": text}]
  104. ack = self.executeCmd(TAG_UI, "clickByText", args)
  105. return self.__checkResult(ack, 'true')
  106. def clickByImage(self, image):
  107. pass
  108. def isScreenOn(self):
  109. ack = self.executeCmd(TAG_UI, "isScreenOn", [])
  110. if ack == "":
  111. return "unknown"
  112. return self.__checkResult(ack, 'true')
  113. def screenOn(self):
  114. self.executeCmd(TAG_UI, "wakeUp", [])
  115. return self.isScreenOn()
  116. def screenOff(self):
  117. self.executeCmd(TAG_UI, "sleep", [])
  118. return not self.isScreenOn()
  119. def goBack(self):
  120. ack = self.executeCmd(TAG_UI, "pressBack", [])
  121. return self.__checkResult(ack, 'true')
  122. def goHome(self):
  123. ack = self.executeCmd(TAG_UI, "pressHome", [])
  124. return self.__checkResult(ack, 'true')
  125. def getTextById(self, id):
  126. args = [{"k": "string", "v": id}]
  127. return self.executeCmd(TAG_UI, "getTextById", args)

三、应用软件(uiautomator封装)

        首先,打开Androdi Studio新建一个Android工程,然后在app文件夹下的build.gradle文件添加依赖库,如下图:

  1. androidTestImplementation 'androidx.test:runner:1.3.0'
  2. androidTestImplementation 'androidx.test:rules:1.3.0'
  3. androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
  4. androidTestImplementation 'androidx.core:core:1.3.0'
  5. androidTestImplementation 'androidx.annotation:annotation:1.1.0'

AndroidManifest.xml中添加如下权限:

  1. <uses-permission android:name="android.permission.INTERNET" />
  2. <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
  3. <uses-permission android:name="android.permission.READ_PHONE_STATE" />
  4. <uses-permission android:name="android.permission.WAKE_LOCK" />
  5. <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  6. <uses-permission android:name="android.permission.GET_ACCOUNTS" />
  7. <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
  8. <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
  9. <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
  10. <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
  11. <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
  12. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  13. <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  14. <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
  15. tools:ignore="ScopedStorage" />

开发完成后,需要打包编译两个APP:app-debug.apk、app-debug-androidTest.apk

app-debug.apk为待测apk,实际上没什么作用,但是需要安装,只要保证能正常运行就行

app-debug-androidTest.apk为测试apk,uiautomator的封装实现就在这个apk里面

启动测试服务:adb shell am instrument -w -r -e debug false -e class com.aiplot.wetest.Stub com.aiplot.wetest.test/androidx.test.runner.AndroidJUnitRunner

adb install命令参数如下

-t 允许测试包
-l 锁定该应用程序
-s 把应用程序安装到sd卡上
-g 为应用程序授予所有运行时的权限
-r 替换已存在的应用程序,也就是说强制安装
-d 允许进行将见状,也就是安装的比手机上带的版本低 

Android源码地址

链接:http://aospxref.com/

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/591390
推荐阅读
相关标签
  

闽ICP备14008679号