Python测开28期-偕行-app自动化高级技能

一、多设备自动化测试

1、应用场景

  • 同个功能,不同的手机或者平台,需要测试是否能够正常运行。

2、 多设备自动化测试方案

image

  • 控制中心:Jenkins、自研平台。

    • 指定设备和用例执行用例
    • 生成测试报告
  • 中控机:STF、Selenium Grid。

    • 中控机:STF
      • 可以远程管理和控制多个设备。
      • 提供设备使用情况和性能数据的实时监控。
    • 中控机:Selenium Grid
      • 分布式测试工具。
      • 在多台设备上并行运行Appium测试,可以快测试速度并提高测试效率。
      • 使用一个中心控制器(hub)来管理测试节点(node),将测试任务分配给可用的节点,并收集测试结果。
  • Hub:供电和数据传输。

  • 移动设备:部署手机端Agent, 自动化测试的执行。

    • 自动遍历工具:monkey、appcrawler。
    • 自动化测试用例执行工具:appium、uiautomator、ATF。
  • 移动设备完成自动化测试的执行。

3、 硬件环境准备

  • 终端机:Ubuntu、Mac
  • usb hub
  • 移动设备:
    • 自购
    • 中小公司10~30
    • 大公司50~200
    • 云端租用

4、多设备管理平台stf(openstf)

github地址

image

docker安装

  • 注意:adb最好还是单独安装并配置环境变量,而不用docker安装adb;
    image

先启动rethinkdb数据库

  • 注意:-v参数指定目录
    image

再启动stf
image

浏览器访问stf

5、跨平台设备管理方案seleniumgrid

(1) Selenium Grid 介绍

  • 介绍:Selenium Grid 是一个智能 代理服务器,它在多台机器上并行运行测试,集中管理不同的浏览器版本和浏览器配置。
  • 作用:专门用于在不同 浏览器操作系统机器并行运行多个测试。
  • 实现方式:通过路由远程浏览器实例的命令来实现的,其中服务器充当集线器。

(2) Selenium Grid 使用场景

  1. 针对不同的浏览器类型、浏览器版本、操作系统并行运行测试。
  2. 减少执行测试套件所需的时间。

(3) Selenium Grid4 新特性

  1. Hub 和 Node 使用同一个 jar 服务。
  2. 架构优化(组件包括:Router、Distributor、Node、Session Map、Session Queue、Event Bus)。
  3. 支持不同的运行模式(Standalone Mode 独立模式、Classical Grid 经典网格、Fully Distributed 完全分布式)。

(4) 运行方式

(5) Selenium Grid 原理分析


  • Router 是 Grid 的入口,接收所有外部请求,并将它们转发给正确的组件。
  • sessionQueue 将会话 ID 映射到会话运行的节点。
  • Distributor 查询新会话队列并处理任何未决的新会话请求,注册并跟踪所有节点及其功能。
  • node 运行测试用例的物理机器节点。

(6) Selenium Grid 环境准备

  • 检查 Java 环境:Java 版本必须 11 或以上。
java -version
javac

(7) 单机运行 - 独立模式

  • 命令行启动 server

      1. 命令行 cd 到当前下载 jar 包的路径下。
      1. java -jar 启动对应的 jar 包:java -jar selenium-server-<version>.jar standalone
      1. 浏览器的 driver 自动下载。
      1. 查看 UI 界面。
      1. 查看 Grid status 状态。
  • 代码运行-单节点:

from time import sleep
from selenium import webdriver
from selenium.webdriver.common.by import By


class TestSingleNode:
    def setup_method(self):
        # 创建Options ,新版本DesireCapability已弃用
        options = webdriver.FirefoxOptions()
        # 通过URL和options 创建一个远程的连接
        self.driver = webdriver.Remote(
            command_executor='http://10.1.1.104:4444',
            options=options
        )

    def test_singlenode1(self):
        # 打开 baidu 页
        self.driver.get("http://www.baidu.com")
        # 向输入框中输入
        self.driver.find_element(By.ID, 'kw').send_keys("firefox")
        # 点击搜索框
        self.driver.find_element(By.ID, 'su').click()
        # 等待一秒
        sleep(1)
        # 断言输入内容在页面中
        assert "firefox" in self.driver.page_source

    def teardown_method(self):
        self.driver.quit()

(8) 代码运行 - 多结点

from time import sleep
from selenium import webdriver
from selenium.webdriver.common.by import By


class TestMultiNode:
    def setup_method(self):
        options = webdriver.ChromeOptions()
        self.driver = webdriver.Remote(
            command_executor='http://10.1.1.104:4444',
            options=options
        )

    def test_multinode1(self):
        # 打开 baidu 页
        self.driver.get("http://www.baidu.com")
        # 向输入框中输入
        self.driver.find_element(By.ID, 'kw').send_keys("selenium")
        # 点击搜索框
        self.driver.find_element(By.ID, 'su').click()
        # 等待一秒
        sleep(1)
        # 断言输入内容在页面中
        assert "selenium" in self.driver.page_source

    def test_multinode2(self):
        self.driver.get("http://www.baidu.com")
        self.driver.find_element(By.ID, 'kw').send_keys("appium")
        self.driver.find_element(By.ID, 'su').click()
        sleep(1)
        assert "appium" in self.driver.page_source

    def test_multinode3(self):
        self.driver.get("http://www.baidu.com")
        self.driver.find_element(By.ID, 'kw').send_keys("pytest")
        self.driver.find_element(By.ID, 'su').click()
        sleep(1)
        assert "pytest" in self.driver.page_source

    def test_multinode4(self):
        self.driver.get("http://www.baidu.com")
        self.driver.find_element(By.ID, 'kw').send_keys("requests")
        self.driver.find_element(By.ID, 'su').click()
        sleep(1)
        assert "requests" in self.driver.page_source

    def test_multinode5(self):
        self.driver.get("http://www.baidu.com")
        self.driver.find_element(By.ID, 'kw').send_keys("java")
        self.driver.find_element(By.ID, 'su').click()
        sleep(1)
        assert "java" in self.driver.page_source

    def teardown_method(self):
        self.driver.quit()

(9) Hub 和 Node 运行 - 经典网格模式

  • 命令行启动 Hub

    • java -jar selenium-server-<version>.jar hub
  • 命令行启动 Node

    • 同一机器上启动 node:java -jar selenium-server-<version>.jar node --detect-drivers true
    • 不同机器上启动 node:java -jar selenium-server-<version>.jar node --detect-drivers true --publish-events tcp://<ip> --subscribe-events tcp://<ip>

二、自动遍历技术

1、 为什么需要自动遍历技术

  • 手工测试的困境

    • 测试广度

      • 回归测试难以保证,测试内容太多导致手工测试无法充分覆盖
      • 兼容性测试难以保证,数十台设备、多种环境、多种版本无法充分覆盖
      • 专项测试回归难度大,内存泄漏、健壮性测试、弱网等测试过程太多
    • 测试深度

      • 人工校验效果难以保证,比如股票相关数据变化共数十个字段,容易产生疏漏
      • 数据搜集繁琐,比如后端接口传输数据的变化和内容收集比较麻烦
    • 测试效能

      • 投入成本大,需要投入大量的人力与管理成本
      • 质量反馈慢,无法实现测试左移与快速反馈
  • 自动化测试的困境

    • 技术门槛高

      • 很多初级工程师对自动化框架掌握程度不够
      • 没有采用 Page Object 模式导致自动化用例维护成本大
      • 没有进行二次框架封装
    • 投入成本大

      • 没有采用合理的 UI 分层测试体系
      • 没有经验丰富的测试开发工程师
  • 我们到底需要什么样的测试方法

    手工测试 自动化测试 理想测试方法
    业务覆盖度 :heart: :heart:
    执行速度 :heart: :heart:
    维护成本 :heart: :heart:
    学习成本 :heart: :heart:

2、 智能遍历测试

  • 将被测系统理解为一个有限状态机,通过遍历的方式达到充分的路径覆盖。
  • 自动遍历测试是基于模型的测试方法的一种应用场景

(1) 基于模型的测试方法

  • 将 app 的业务行为理解为一个有向图
  • 有向图中的节点代表业务状态
  • 有向图中的路径代表达到特定状态的行为
  • 以充分遍历所有状态为目标

(2) 智能遍历测试的特点

(3) 智能遍历测试相关的工具

3、androidmaxim遍历测试工具

(1) maxim 介绍

An efficient Android Monkey Tester, available for emulators and real devices 基于遍历规则的高性能 Android Monkey,适用于真机/模拟器的 APP UI 压力测试.

(2)环境准备

image

  • 支持 Android 5,6,7,8,9、10、11真机及模拟器;
  • 将 framework.jar , monkey.jar push 到手机上某个目录中,建议/sdcard
adb push framework.jar /sdcard
adb push monkey.jar /sdcard

(3) 命令行模式

  • cmd 命令 : adb shell CLASSPATH=/sdcard/monkey.jar:/sdcard/framework.jar exec app_process /system/bin tv.panda.test.monkey.Monkey -p com.panda.videoliveplatform --uiautomatormix --running-minutes 60 -v -v

    • CLASSPATH:指定monkey.jar和framework.jar的路径;
    • exec app_process /system/bin:固定写法;
    • tv.panda.test.monkey.Monkey: monkey入口类,不要修改;
    • -p com.panda.videoliveplatform: 被测app包名,需要修改;
    • --uiautomatormix: 遍历策略;
    • --running-minutes 60 -v -v:monkey命令中的参数;

(4) 策略

  1. 模式 Mix (基于事件概率的压力测试)
--uiautomatormix
直接使用底层accessibiltyserver获取界面接口 解析各控件,随机选取一个控件执行touch操作。
  同时与原monkey 其他操作按比例混合使用
  默认accessibilityserver action占比50%,其余各action分剩余的50%
  accessibilityserver action占比可配置 --pct-uiautomatormix n
  1. 模式 DFS
--uiautomatordfs
深度遍历算法
  1. 模式Troy
--uiautomatortroy
控件选择策略按max.xpath.selector配置的高低优先级来进行深度遍历
  1. 保留原始monkey

  2. 总运行时长 --running-minutes 3 运行3分钟

  3. –act-whitelist-file /sdcard/awl.strings 定义白名单 --act-blacklist-file

  4. 其他参数与原始monkey一致

4、androidfastbot遍历测试工具

(1) Fastbot 介绍

基于 model-based testing 结合机器学习、强化学习的 APP 稳定性测试工具。

Fastbot is a model-based testing tool for modeling GUI transitions to discover app stability problems. It combines machine learning and reinforcement learning techniques to assist discovery in a more intelligent way.

(2)Fastbot架构图

(3) fastbot_android quick start

git clone https://github.com/bytedance/Fastbot_Android.git
cd Fastbot_Android
adb push *.jar /sdcard
adb push libs/* /data/local/tmp/
adb shell CLASSPATH=/sdcard/monkeyq.jar:/sdcard/framework.jar:/sdcard/fastbot-thirdpart.jar exec app_process /system/bin com.android.commands.monkey.Monkey -p io.appium.android.apis  --agent robot  --throttle 200  -v -v 1000

5、appcrawler自动遍历工具

Appcrawler 是一个基于自动遍历的App爬虫工具,支持 Android 和 IOS,支持真机和模拟器。最大的特点是灵活性高,可通过配置来设定遍历的规则。

(1)appcrawler的诞生背景

  • 业务常见问题

    • 功能问题

      • app某界面崩溃
      • app某接口有报错
      • 详情页中特定信息字段内容丢失或者数据异常
      • 微信分享不可用
    • 兼容性问题

      • 用户网络慢时发出请求后退出当前页面发生崩溃
      • 某些界面在4.4和5.0的系统上操作体验不同
  • 测试痛点

    • 快速迭代中传统的基于用例维护的自动化用例使用不当

      • 没有采用合理的分层测试体系,尝试用UI自动化覆盖所有测试场景
      • 没有采用PageObject模式导致自动化用例维护成本大
      • 对自动化框架掌握程度不够
    • 测试内容太多导致手工测试无法充分覆盖

      • UI自动化只能覆盖核心业务逻辑,新功能来不及上自动化
      • 产品业务测试量较大,新版发布后,老功能来不及全面回归,容易漏测
      • 时间长,强度大的工作后,人容易产生疲乏,对数字的位数,文字的显示等错误信息的敏感度下降
      • 产品的界面深度很深,且包含大量的展示信息功能
      • 专项测试回归难度大:内存泄漏、健壮性测试、弱网等测试太多

(2)appcrawler使用

A、环境准备

B、使用步骤

  • 1、启动appium服务

  • 2、运行appcrawler,会自动生成报告

    • 简单启动:java -jar <appcrawler.jar路径> --capability "appPackage=com.xueqiu.android,appActivity=.view.WelcomeActivityAlias"
    • 使用配置文件启动:java -jar <appcrawler.jar路径> -c example.yml
  • 3、从已经结束的结果中重新生成报告:appcrawler --report result/

(3)appcrawler命令行参数

Usage: appcrawler [options]

  -a, --app <value>        Android或者iOS的文件地址, 可以是网络地址, 赋值给appium的app选项
  -e, --encoding <value>   set encoding, such as UTF-8 GBK
  -c, --conf <value>       配置文件地址
  -p, --platform <value>   平台类型android或者ios, 默认会根据app后缀名自动判断
  -t, --maxTime <value>    最大运行时间. 单位为秒. 超过此值会退出. 默认最长运行3个小时
  -u, --appium <value>     appium的url地址
  -o, --output <value>     遍历结果的保存目录. 里面会存放遍历生成的截图, 思维导图和日志
  --capability k1=v1,k2=v2...
                           appium capability选项, 这个参数会覆盖-c指定的配置模板参数, 用于在模板配置之上的参数微调
  -y, --yaml <value>       代表配置的yaml语法,比如blackList: [ {xpath: action_night } ],用于避免使用配置文件的情况
  -r, --report <value>     输出html和xml报告
  --template <value>       输出代码模板
  --master <value>         master的diff.yml文件地址
  --candidate <value>      candidate环境的diff.yml文件
  -v, --verbose-debug      是否展示更多debug信息
  -vv, --verbose-trace     是否展示更多trace信息
  --demo                   生成demo配置文件学习使用方法
  --help
                           示例
                           appcrawler -a xueqiu.apk
                           appcrawler -a xueqiu.apk --capability noReset=true
                           appcrawler -c conf/xueqiu.json -p android -o result/
                           appcrawler -c xueqiu.yaml --capability udid=[你的udid] -a Snowball.app
                           appcrawler -c xueqiu.yaml -a Snowball.app -u 4730
                           appcrawler -c xueqiu.yaml -a Snowball.app -u http://127.0.0.1:4730/wd/hub

                           #生成demo配置文件到当前目录下的demo.yaml
                           appcrawler --demo

                           #启动已经安装过的app
                           appcrawler --capability "appPackage=com.xueqiu.android,appActivity=.view.WelcomeActivityAlias"

                           #使用yaml参数
                           appcrawler -a xueqiu.apk -y "blackList: [ {xpath: action_night}, {xpath: '.*[0-9\\.]{2}.*'} ]"

                           #从已经结束的结果中重新生成报告
                           appcrawler --report result/

(4)appcrawler配置文件yaml参数

执行参数与配置文件

  • capability设置:与appium完全一致
  • testcase:用于启动app后的基础测试用例
  • selectedList:遍历范围设定
  • triggerActions:特定条件触发执行动作的设置
  • selectedList:需要被遍历的元素范围
  • firstList:优先被点击
  • lastList:最后被点击
  • tagLimitMax:同祖先(同类型)的元素最多点击多少次
  • backButton:当所有元素都被点击后默认后退控件定位
  • blackList:黑名单
  • maxDepth: 遍历的最大深度

配置的最小单元 测试用例模型

testcase的完整形态

  • given:所有的先决条件
  • when:先决条件成立后的行为
  • then:断言集合

testcase的简写形态

  • xpath:对应when里的xpath
  • action:对应when的action

执行参数比配置文件优先级别高

  • given 前提条件
  • when 执行动作
  • then 写断言

简写形态

  • xpath xpath支持xpath表达式、正则、包含
  • action 支持

xpath定义

  • xpath
    • //*[@resource-id=‘xxxx’]
    • //*[contains(@text, ‘密码’)]
  • 正则
    • ^确定$
    • ^.*输入密码
  • 包含
    • 密码
    • 输入

action定义

  • “” 只是截图记录
  • back 后退
  • backApp 回退到当前的app 默认等价于back行为 可定制
  • monkey 随机事件
  • click
  • longTap
  • xxx() 执行scala或者java代码
    • Thread.sleep(3000)
    • driver.swipe(0.9, 0.5, 0.1, 0.5)
  • 非以上所有行为是输入 xx ddd

完整配置文件:

---
maxTimeDescription: "最大运行时间"
maxTime: 10800
maxDepthDescription: "默认的最大深度10, 结合baseUrl可很好的控制遍历的范围"
maxDepth: 10
capabilityDescription: "appium的capability通用配置,其中automationName代表自动化的驱动引擎,除了支持appium的所有引擎外,额外增加了adb和selenium的支持"
capability:
  appActivity: ".ApiDemos"
  appium: "http://127.0.0.1:4723/wd/hub"
  noReset: "true"
  appPackage: "io.appium.android.apis"
  fullReset: "false"
testcaseDescription: "测试用例设置,用于遍历开始之前的一些前置操作,比如自动登录"
testcase:
  name: "AppCrawler TestCase"
  steps:
    - given: []
      when: null
      then: []
      xpath: "/*/*"
      action: "Thread.sleep(1000)"
      actions: []
      times: -1
triggerActionsDescription: "在遍历过程中需要随时处理的一些操作,比如弹框、登录等"
triggerActions:
  - given: []
    when: null
    then: []
    xpath: "permission_allow_button"
    action: ""
    actions: []
    times: 3
  - given: []
    when: null
    then: []
    xpath: "允许"
    action: ""
    actions: []
    times: 3
selectedListDescription: "默认遍历列表,只有出现在这个列表里的控件范围才会被遍历"
selectedList:
  - given: []
    when: null
    then: []
    xpath: "//*[contains(name(), 'Button')]"
    action: ""
    actions: []
    times: -1
  - given: []
    when: null
    then: []
    xpath: "//*[contains(name(), 'Text') and @clickable='true' and string-length(@text)<10]"
    action: ""
    actions: []
    times: -1
  - given: []
    when: null
    then: []
    xpath: "//*[@clickable='true']//*[contains(name(), 'Text') and string-length(@text)<10]"
    action: ""
    actions: []
    times: -1
  - given: []
    when: null
    then: []
    xpath: "//*[contains(name(), 'Image') and @clickable='true']"
    action: ""
    actions: []
    times: -1
  - given: []
    when: null
    then: []
    xpath: "//*[@clickable='true']/*[contains(name(), 'Image')]"
    action: ""
    actions: []
    times: -1
  - given: []
    when: null
    then: []
    xpath: "//*[contains(name(), 'Image') and @name!='']"
    action: ""
    actions: []
    times: -1
  - given: []
    when: null
    then: []
    xpath: "//*[contains(name(), 'Text') and @name!='' and string-length(@label)<10]"
    action: ""
    actions: []
    times: -1
  - given: []
    when: null
    then: []
    xpath: "//a"
    action: ""
    actions: []
    times: -1
  - given: []
    when: null
    then: []
    xpath: "//*[contains(@class, 'Text') and @clickable='true' and string-length(@text)<10]"
    action: ""
    actions: []
    times: -1
  - given: []
    when: null
    then: []
    xpath: "//*[@clickable='true']//*[contains(@class, 'Text') and string-length(@text)<10]"
    action: ""
    actions: []
    times: -1
  - given: []
    when: null
    then: []
    xpath: "//*[contains(@class, 'Image') and @clickable='true']"
    action: ""
    actions: []
    times: -1
  - given: []
    when: null
    then: []
    xpath: "//*[@clickable='true']/*[contains(@class, 'Image')]"
    action: ""
    actions: []
    times: -1
  - given: []
    when: null
    then: []
    xpath: "//*[@clickable='true' and contains(@class, 'Button')]"
    action: ""
    actions: []
    times: -1
blackListDescription: "黑名单列表 matches风格, 默认排除内容包含2个数字的控件"
blackList:
  - given: []
    when: null
    then: []
    xpath: ".*[0-9]{2}.*"
    action: ""
    actions: []
    times: -1
firstListDescription: "优先遍历列表,同时出现在selectedList与firstList中的控件会被优先遍历"
firstList: []
lastListDescription: "最后遍历列表,同时出现在selectedList与lastList中的控件会被最后遍历"
lastList:
  - given: []
    when: null
    then: []
    xpath: "//*[@selected='true']/..//*"
    action: ""
    actions: []
    times: -1
  - given: []
    when: null
    then: []
    xpath: "//*[@selected='true']/../..//*"
    action: ""
    actions: []
    times: -1
backButtonDescription: "后退按钮列表,默认在所有控件遍历完成后,才会最后点击后退按钮。目前具备了自动判断返回按钮的能力,默认不需要配置"
backButton:
  - given: []
    when: null
    then: []
    xpath: "Navigate up"
    action: ""
    actions: []
    times: -1
xpathAttributesDescription: "在生成一个控件的唯一定位符中应该包含的关键属性"
xpathAttributes:
  - "name()"
  - "name"
  - "label"
  - "value"
  - "resource-id"
  - "content-desc"
  - "text"
  - "id"
  - "name"
  - "innerText"
  - "tag"
  - "class"
sortByAttributeDescription: "陆续根据属性进行遍历排序微调,depth表示从dom中最深层的控件开始遍历,list表示dom中列表优先,\
  selected表示菜单最后遍历,这是默认规则,一般不需要改变"
sortByAttribute:
  - "depth"
  - "list"
  - "selected"
findByDescription: "默认生成控件唯一定位符的表达式风格,可选项 default|android|id|xpath,默认会自动判断是否使用android定\
  位或者ios定位"
findBy: "xpath"
baseUrlDescription: "设置一个起始点,从这个起始点开始计算深度,比如默认从登录后的界面开始计算"
baseUrl: []
appWhiteListDescription: "app白名单,允许在这些app里进行遍历"
appWhiteList: []
urlBlackListDescription: "url黑名单,用于排除某些页面的遍历"
urlBlackList: []
urlWhiteListDescription: "url白名单,仅在这些界面内遍历"
urlWhiteList: []
beforeStartWaitDescription: "启动一个app默认等待的时间"
beforeStartWait: 6000
beforeRestart: []
beforeElementDescription: "在遍历每个控件之前默认执行的动作"
beforeElement: []
afterElementDescription: "在遍历每个控件之后默认执行的动作"
afterElement: []
afterElementWaitDescription: "在遍历每个控件之后默认等待的时间,用于等待新页面加载"
afterElementWait: 500
afterAllDescription: "在遍历完当前页面内的所有控件后,是否需要刷新或者滑动"
afterAll: []
afterAllMaxDescription: "afterAll的最大重试次数,比如连续滑动2次都没新元素即取消"
afterAllMax: 2
tagLimitMaxDescription: "相似控件最多点击几次"
tagLimitMax: 2
tagLimitDescription: "设置部分相似控件的最大遍历次数"
tagLimit:
  - given: []
    when: null
    then: []
    xpath: "确定"
    action: ""
    actions: []
    times: 1000
  - given: []
    when: null
    then: []
    xpath: "取消"
    action: ""
    actions: []
    times: 1000
  - given: []
    when: null
    then: []
    xpath: "share_comment_guide_btn_name"
    action: ""
    actions: []
    times: 1000
assertGlobalDescription: "全局断言"
assertGlobal: []
suiteNameDescription: "报告中的测试套件名字可以由列表内的控件内容替换,增强报告中关键界面的辨识度"
suiteName:
  - "//*[@selected='true']//android.widget.TextView/@text"
screenshotDescription: "是否截图"
screenshot: true
reportTitleDescription: "报告的title"
reportTitle: "AppCrawler"
resultDirDescription: "结果目录,如果为空会自动创建对应时间戳_报名的结果目录"
resultDir: ""
showCancelDescription: "是否展示跳过的控件记录"
showCancel: true
pluginListDescription: "插件列表,暂时禁用,太高级了,很多人不会用"
Description: "。在selectedList firstList lastList等很多配置中,需要填充的是测试步骤Step类型。Step类型由given(\
  满足条件)when(条件满足的行为)then(断言)三部分组成。Step可以简化为xpath(定位表达式,支持xpath 正则 包含关系)与action(点击\
  \ 输入等行为)。"
pluginList: []

三、基于手机短信验证码的自动化测试

1、 短信验证码简介

  • 短信验证码是通过发送验证码到手机的一种有效的验证码系统。
  • 可以比较准确和安全地保证系统的安全性,验证用户的正确性。
  • 利用短信验证码来注册,大大降低了非法注册,烂注册的数据。

2、 短信验证码自动化测试方案

  1. 研发给暗门或者万能验证码。
  2. 通过appium获取通知栏中短信内容(appium版本2.16.0以上)。
  3. 其他方案(较麻烦):
  • 通过短信app获取短信内容。
  • 通过短信数据库SQLite获取短信内容,需要root权限和SQLite客户端。
  • 通过广播服务监听短信内容,需要开发app,注册监听器。
  • 通过service服务监听界面所有信息流,需要开发app,注册accessbilityservice。

3、通过appium获取通知栏中短信内容

(1)华为手机adb连接

  • 1、打开开发者模式,然后打开USB调试;

  • 2、打开拨打电话界面 输入(*#*#2846579#*#*) ,打开后台设置 → usb端口设置 → 生产模式,然后尝试连接 (adb connect 192.168.*.***:5555);

  • 3、如果连接不成功 PC端打开cmd命令窗口,输入adb devices 查看已经连接的设备,如果未显示设备就杀掉adb:adb kill-server,再重启adb:adb start-server,再查看设备,设备出现之后输入 (adb tcpip 8888) 把端口号设置为 8888,然后尝试连接(adb connect 192.168.***.***:8888);
    image
    image

  • 4、appium启动华为手机
    华为(鸿蒙系统)貌似归于Android一类的,所以配置信息中,platformName是Android。

C:\Users\DELL>adb shell getprop ro.build.version.release
10

C:\Users\DELL>adb devices -l
List of devices attached
5GK4C19A14007056       device product:MAR-AL00 model:MAR_AL00 device:HWMAR transport_id:1


C:\Users\DELL>adb shell dumpsys activity | findstr "mResume"
    mResumedActivity: ActivityRecord{3fb91b2 u0 com.tencent.wework/.launch.LaunchSplashActivity t533}

说明:

第一个命令结果是10,即Android系统版本(platformVersion)是10;

第二个命令查看设备的详细信息,MAR-AL00 是设备名(deviceName)

第三个命令可以找到你当前手机正在打开的应用信息,也就是说你要测什么app就提前打开这个app,然后通过第三个命令获取这个app的信息,后面appium连接的时候就会打开这个app。com.tencent.wework就是你对应appPackage,.launch.LaunchSplashActivity 就是对应的appActivity。

(2)案例

  • 需求:通过短信验证码方式登录企业微信

sms_util.py

import subprocess

def get_smscode_by_adb_01():
    """
    通过adb命令,使用appium获取通知栏数据--TODO
    :return: 
    """
    # 通过adb命令,使用appium获取通知栏数据
    cmd = "adb shell broadcast -a io.appium.settings.notifications"
    # 创建一个子进程去执行adb命令并返回结果
    result = subprocess.Popen(
        # 执行的cmd命令
        cmd,
        # 输出
        stdout=subprocess.PIPE,
        # 容错
        stderr=subprocess.STDOUT,
        # 执行shell
        shell=True
    ).stdout.readlines() # 输出并读取多行
    # 手机中没找到/system/bin/sh目录--TODO
    print(f"result={str(result[0],'utf-8')}") # result=/system/bin/sh: broadcast: inaccessible or not found

    # 对短信内容进行切片,获取最后一个验证码
    """
    【腾讯科技】你正在登录企业微信App,验证码请勿告知他人,泄露会导致企业微信账号被盗,使企业资产受到损失。验证码:468789 ,5分钟内有效。
    1、将adb执行结果转成字符串并进行中文编码
    2、使用冒号分隔取取下标为1的元素得到:468789 ,5分钟内有效。
    3、再用逗号分隔取下标为0的元素得到:468789 
    4、再去掉两边的空格得到完整验证码:468789
    """
    smscode = str(result[1],'utf-8').split(":")[1].split(",")[0].strip()
    print(f"smscode={smscode}")

    return smscode

def get_smscode_by_adb_02():
    """
    通过adb读取短线app内容获取短信数据
    :return: 
    """
    # # 执行adb命令连接手机
    # # adb devices
    # subprocess.call(['adb','devices'])
    # # 执行adb命令打开短信app
    # # adb shell am start -n com.android.mms/.ui.ConversationList
    # subprocess.call(['adb','shell','am','start','-n','com.android.mms/.ui.ConversationList'])

    # 执行adb命令获取最新的一条短信
    # adb shell content query --uri content://sms/
    # adb shell content query --uri content://sms/inbox --projection body --结果顺序为手机短信列表的顺序,一般是按时间从近往远排列
    """
    --uri content://sms/inbox 指定查询短信内容提供器的收件箱
    --projection body 只查询短信的 body,也就是内容列
    """
    cmd = "adb shell content query --uri content://sms/inbox --projection body"

    # 创建一个子进程去执行adb命令
    p = subprocess.Popen(
        # 执行的cmd命令
        cmd,
        # 输出
        stdout=subprocess.PIPE,
        # 容错
        stderr=subprocess.STDOUT,
        # 执行shell
        shell=True
    )
    # 返回执行adb命令结果
    output = p.stdout.readlines()
    print(type(output)) # <class 'list'>

    # # 对短信内容进行切片,获取最近一个验证码
    """
    Row: 1 body=【腾讯科技】你正在登录企业微信App,验证码请勿告知他人,泄露会导致企业微信账号被盗,使企业资产受到损失。验证码:136070 ,5分钟内有效。

    1、将adb执行结果转成字符串并进行中文编码
    2、使用冒号分隔取取下标为1的元素得到:136070 ,5分钟内有效。
    3、再用逗号分隔取下标为0的元素得到:136070  
    4、再去掉两边的空格得到完整验证码:136070
    """
    smscode = str(output[0],'utf-8').split(":")[1].split(",")[0].strip()
    print(f"smscode={smscode}")

    # 关闭进程--如果不关闭,会造成进程强制资源导致appium的进程挂掉,session被干掉之后后续操作无法进行
    p.kill()
    # 等待进程关闭
    p.wait(timeout=5)

    return smscode

if __name__ == '__main__':
    get_smscode_by_adb_02()

testwework_sms.py

import time
from appium import webdriver
from appium.options.common import AppiumOptions
from appium.webdriver.common.appiumby import AppiumBy
from smscode.sms_utils import get_smscode_by_adb_02

# capability配置
caps = {
    "platformName":"Android",
    "appium:platformVersion":"10",
    "appium:deviceName":"MAR-AL00",
    "appium:appPackage":"com.tencent.wework",
    "appium:appActivity":".launch.LaunchSplashActivity",
    "appium:automationName":"UiAutomator2",
    # "appium:noReset":"true", # 不清理缓存这里不要用,因为手机端会自动清理缓存,如果使用了清理缓存,用inspector定位的时候输入手机号界面不一样
    "appium:unicodeKeyboard": "true",
    "appium:restKeyboard": "true",
    # 配合noReset使用,不然无法启动app
    "appium:forceAppLaunch":"true",
    # 配合driver.quit()使用,不然无法退出app
    "appium:shouldTerminateApp":"true"
}
# appium服务器地址
appium_server_url = 'http://127.0.0.1:4723'
driver = webdriver.Remote(appium_server_url,options=AppiumOptions().load_capabilities(caps=caps))
driver.implicitly_wait(10)

# 点击隐私保护指引弹窗同意
driver.find_element(AppiumBy.ID,'com.tencent.wework:id/ct_').click()

# 点击手机号登录
driver.find_element(AppiumBy.ID,'com.tencent.wework:id/inl').click()
time.sleep(1)
print(1)

# 点击同意协议
driver.find_element(AppiumBy.ID,'com.tencent.wework:id/cta').click()
time.sleep(1)
print(2)

# 输入手机号--有时会自动识别本机手机号,所以在定位手机号输入框的时候会出现异常,处理异常
try:
    phone = driver.find_element(AppiumBy.ID, 'com.tencent.wework:id/inv')
    phone.clear()
    phone.send_keys("15885417532")
    time.sleep(1)
    print(3)
    # 点击下一步
    driver.find_element(AppiumBy.ID, 'com.tencent.wework:id/f7').click()
except Exception as e:
    print(e)
    # 出现异常说明是自动识别了本机手机号,不需要手动输入,直接点击登录
    driver.find_element(AppiumBy.ID,'com.tencent.wework:id/ahb').click()

# 获取手机短信中的验证码,强制等待50s,等验证码发送到手机
time.sleep(50)
smscode = get_smscode_by_adb_02()
print(f"短信验证码:{smscode}")

# 输入手机短信验证码
driver.find_element(AppiumBy.ID,"com.tencent.wework:id/fjl").send_keys(smscode)

# 点击下一步
driver.find_element(AppiumBy.ID,"com.tencent.wework:id/f7").click()

time.sleep(30)
driver.quit()

四、基于ocr识别方法的自动化测试–图片验证码处理

1、 OCR 文字识别简介

  • 光符识别(optical character recognition)。
  • 对文本资料进行扫描,然后对图像文件进行分析处理,获取文字及版面信息的过程。

2、 常见OCR识别第三方库

  • EasyOCR
    • 基于Tesseract的OCR识别库,用于图像识别输出文本,目前支持80多种语言。
    • 模型包含文本检测、文本识别功能。
    • 安装:pip install easyocr
  • MuggleOCR
    • 一款轻量级的ocr识别库,从名字也可以看出来,专为麻瓜设计。
    • 使用非常简单。
    • 主要是用于识别各类验证码,一般文字提取效果就稍差了。
    • 安装:pip install muggle-ocr
    • 注意:目前貌似已经不能用了,估计放弃了开源
  • ddddOCR
    • 一款用于识别验证码的开源库。
    • 对一些常规的数字、字母验证码识别有奇效
    • 安装:pip install ddddocr

3、案例

(1)easyocr

"""
使用easyocr获取图片验证码
"""
import os
import time
import requests
from easyocr import easyocr
from selenium import webdriver
from selenium.webdriver.common.by import By
from  PIL import Image


def get_code_by_picurl(picurl,picname):
    """
    通过验证码图片链接获取验证码
    :param picurl:
    :return:
    """
    # 发送请求获取图片
    picture = requests.get(picurl)
    # 保存图片到本地
    with open(picname,"wb") as f:
        f.write(picture.content)
    # 写入需要时间,等待1s
    time.sleep(1)
    # 通过本地图片获取验证码
    return get_code_by_picture(picname)

def get_code_by_screenshot(driver,locator):
    """
    通过验证码图片定位器获取验证码
    :param driver:
    :param locator:
    :return:
    """
    driver.save_screenshot("screenshot.png")
    code_ele = driver.find_element(*locator)
    # 获取元素坐标
    location = code_ele.location
    # 获取元素尺寸
    size = code_ele.size
    # 获取元素的四个点坐标--抠图太小识别不准,高和宽各加10个px
    left = location['x']-10
    top = location['y']-10
    right = left + size['width']+20
    bottom = top + size['height']+20
    # 从截图中抠出验证码图片
    screen_image = Image.open("screenshot.png")
    print((left, top, right, bottom))
    code_image = screen_image.crop((left, top, right, bottom))
    # 图像实例对象的一个方法,接受一个 mode 参数,用以指定一种色彩模式
    code_image = code_image.convert('RGB')  # RGB: 3x8位像素,真彩色
    # 保存抠出的验证码图片
    picname = "code_image.png"
    code_image.save(picname)
    # 删除屏幕截图
    # os.remove("screenshot.png")
    # 通过本地图片获取验证码
    return get_code_by_picture(picname)

def get_code_by_picture(picname):
    """
    通过本地图片获取验证码
    :param picname:
    :return:
    """
    # 实例化easyocr参数为一个列表,ch_sim为支持简体中文读取,en为支持英文读取
    ocr = easyocr.Reader(['ch_sim','en'])
    # 识别图片内容
    result = ocr.readtext(picname)
    # print(f"result={result}") # result=[([[50, 45], [377, 45], [377, 162], [50, 162]], '7 364', 0.8456359799914044)]
    # 去掉空格--整体为一个列表,列表中有一个元组,元组的第二个元素就是验证码
    code = result[0][1].replace(" ","")
    # 删除图片
    # os.remove(picname)
    return code

if __name__ == '__main__':
    # 通过本地图片获取验证码
    letter_code = get_code_by_picture("letter_code.png")
    print(f"get_code_by_picture字母验证码为:{letter_code}")
    number_code = get_code_by_picture("number_code.png")
    print(f"get_code_by_picture数字验证码为:{number_code}")

    #通过验证码图片链接获取验证码
    number_code_url = "https://ceshiren.com/uploads/default/original/3X/b/7/b7507d001e141b65ba7125dde50b5565c602b69e.png"
    letter_code_url = "https://ceshiren.com/uploads/default/original/3X/0/5/05acf77acc9eb60140f9322e2d504fe2acb03672.png"
    letter_code = get_code_by_picurl(letter_code_url,"letter_code_url.png")
    print(f"get_code_by_picurl字母验证码为:{letter_code}")
    number_code = get_code_by_picurl(number_code_url,"number_code_url.png")
    print(f"get_code_by_picurl数字验证码为:{number_code}")

    # 通过验证码图片定位器获取验证码
    # 使用selenium打开网页
    driver = webdriver.Chrome()
    driver.get("https://vip.ceshiren.com/#/ui_study/code")
    driver.maximize_window()
    driver.implicitly_wait(10)

    # 数字://*[@class="ma-1 box code"]/div/div/img
    # 字母://*[@class="ma-1 box code"]/div[2]/div/img
    number_code_locator = (By.XPATH,'//*[@class="ma-1 box code"]/div/div/img')
    letter_code_locator = (By.XPATH,'//*[@class="ma-1 box code"]/div[2]/div/img')

   letter_code = get_code_by_screenshot(driver,letter_code_locator)
    print(f"get_code_by_screenshot字母验证码为:{letter_code}") # get_code_by_screenshot字母验证码为:StsD
    number_code = get_code_by_screenshot(driver,number_code_locator)
    print(f"get_code_by_screenshot数字验证码为:{number_code}") # get_code_by_screenshot数字验证码为:736

    driver.quit()

(2)ddddocr

import ddddocr

def get_code_by_picture(picname):
    """
    通过本地图片获取验证码
    :param picname:
    :return:
    """
    # 实例化ddddocr
    ocr = ddddocr.DdddOcr()
    # 识别图片内容
    with open(picname,"rb") as f:
        ima_bytes = f.read()
        code = ocr.classification(ima_bytes)
    return code

if __name__ == '__main__':
    # 通过本地图片获取验证码
    letter_code = get_code_by_picture("letter_code.png")
    print(f"get_code_by_picture字母验证码为:{letter_code}") # get_code_by_picture字母验证码为:stsd
    number_code = get_code_by_picture("number_code.png")
    print(f"get_code_by_picture数字验证码为:{number_code}") # get_code_by_picture数字验证码为:7364

五、基于ai识别方法的自动化测试–图片验证码

1、 AI 识别简介

  • AI(Artificial Intelligence),指的是人工智能
  • 提取处理图像
    • 特征检测
    • 图像分割
    • 图像恢复
    • 图像压缩

2、 常见 AI 识别深度学习框架

  • OpenCV
    • 跨平台计算机视觉和机器学习软件库。
    • 实现了图像处理和计算机视觉方面的很多通用算法。
  • PyTorch
    • Facebook 开发的一个开源深度学习框架。
    • 开源的 Python 机器学习库。
  • TensorFlow
    • Google Brain 团队开发的一个开源深度学习框架。
    • 基于数据流编程的符号数学系统。
    • 用于各类机器学习算法的编程实现。

3、案例,使用OpenCV处理图片增加easyocr的准确度

"""
使用OpenCV+easyocr提高ocr的识别准确度
"""
import cv2
from PIL import Image
from easyocr import easyocr
from selenium import webdriver
from selenium.webdriver.common.by import By

def opencv_image(picname,opencv_img_name):
    """
    使用ai对图片进行处理
    :param picname:
    :return:
    """
    # 读取图片
    image = cv2.imread(picname)
    # 边缘保留滤波,去噪
    image1 = cv2.pyrMeanShiftFiltering(image, sp=8, sr=60)
    # 灰度图像,颜色处理
    image2 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)
    # 颜色取反
    cv2.bitwise_not(image2, image2)
    # 保存OpenCV处理后的图片
    cv2.imwrite(opencv_img_name, image2)

def get_code_by_picture(picname):
    """
    通过本地图片获取验证码
    :param picname:
    :return:
    """
    # 使用OpenCV处理图片
    opencv_image(picname,"opencv_code_image.png")
    # 实例化easyocr参数为一个列表,ch_sim为支持简体中文读取,en为支持英文读取
    ocr = easyocr.Reader(['ch_sim','en'])
    # 识别图片内容
    result = ocr.readtext("opencv_code_image.png")
    # print(f"result={result}") # result=[([[50, 45], [377, 45], [377, 162], [50, 162]], '7 364', 0.8456359799914044)]
    # 去掉空格--整体为一个列表,列表中有一个元组,元组的第二个元素就是验证码
    code = result[0][1].replace(" ","")
    # 删除图片
    # os.remove(picname)
    return code

def get_code_by_screenshot(driver,locator):
    """
    通过验证码图片定位器获取验证码
    :param driver:
    :param locator:
    :return:
    """
    driver.save_screenshot("screenshot.png")
    code_ele = driver.find_element(*locator)
    # 获取元素坐标
    location = code_ele.location
    # 获取元素尺寸
    size = code_ele.size
    # 获取元素的四个点坐标
    left = location['x']
    top = location['y']
    right = left + size['width']
    bottom = top + size['height']
    # 从截图中抠出验证码图片
    screen_image = Image.open("screenshot.png")
    print((left, top, right, bottom))
    code_image = screen_image.crop((left, top, right, bottom))
    # 图像实例对象的一个方法,接受一个 mode 参数,用以指定一种色彩模式
    code_image = code_image.convert('RGB')  # RGB: 3x8位像素,真彩色
    # 保存抠出的验证码图片
    picname = "code_image.png"
    code_image.save(picname)
    # 删除屏幕截图
    # os.remove("screenshot.png")
    # 通过本地图片获取验证码
    return get_code_by_picture(picname)

if __name__ == '__main__':


    # 通过验证码图片定位器获取验证码--使用OpenCV处理图片
    # 使用selenium打开网页
    driver = webdriver.Chrome()
    driver.get("https://vip.ceshiren.com/#/ui_study/code")
    driver.maximize_window()
    driver.implicitly_wait(10)

    # 数字://*[@class="ma-1 box code"]/div/div/img
    # 字母://*[@class="ma-1 box code"]/div[2]/div/img
    number_code_locator = (By.XPATH,'//*[@class="ma-1 box code"]/div/div/img')
    letter_code_locator = (By.XPATH,'//*[@class="ma-1 box code"]/div[2]/div/img')

    letter_code = get_code_by_screenshot(driver,letter_code_locator)
    print(f"使用OpenCV处理图片后字母验证码为:{letter_code}")# 使用OpenCV处理图片后字母验证码为:StsD
    number_code = get_code_by_screenshot(driver,number_code_locator)
    print(f"使用OpenCV处理图片后数字验证码为:{number_code}")# 使用OpenCV处理图片后数字验证码为:7364

    driver.quit()

六、app反编译

1、 为什么要掌握 app 反编译

  • 分析资源使用情况,比如方法数、资源大小
  • 安全检查,比如密钥、敏感注释
  • 辅助测试,比如用例生成、界面与控件数统计

2、 Android 资源打包工具 aapt 与 aapt2

aapt 可以分析 apk 基本内部结构

aapt2 dump badging demo.apk
aapt2 dump xmltree demo.apk --file r

3、 apk 反编译与重新编译 apktool

一个第三方工具,可以对apk进行反编译和重新编译,稳定性上不是很好。

# d是反编译
$ apktool d test.apk
I: Using Apktool 2.6.0 on test.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: 1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
# b是重新编译
$ apktool b test
I: Using Apktool 2.6.0 on test
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
I: Checking whether resources has changed...
I: Building resources...
I: Building apk file...
I: Copying unknown files/dir...

4、 dex 反编译为 java 源代码 jadx

5、 apk 反编译综合 IDE工具 Apk Studio

  • Cross platform, run on Linux, Mac OS X & Windows
  • Decompile/recompile/sign & install APKs
  • Built-in code editor (.java; .smali; .xml; .yml) w/ syntax highlighting
  • Built-in viewer for image (.gif; .jpg; .jpeg; .png) files
  • Built-in hex editor for binary files

七、app测试用例自动生成技术–TODO

八、app端代码mock与hook

1、app端代码mock-代理(Charles/Mitmproxy)

2、app端代码hook

(1)为什么需要hook

App 开发和测试中,如何模拟一些数据或者行为来测试应用程序的响应和健壮性?

(2)什么是hook

钩子编程(hooking),也称作“挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术处理被拦截的函数调用、事件、消息的代码,被称为钩子(hook)。

Hook原意是指钩子,它表示的就是在某个函数的上下文做自定义的处理来实现我们想要的黑科技。

(3)hook大概原理

创建一个代理对象,然后把原始对象替换为我们的代理对象,这样就可以在这个代理对象为所欲为,修改参数或替换返回值。

正常调用:
image

HOOK调用:

(4)代码hook技术

  • 程序运行时,通过修改或替换应用程序的函数或方法,来实现对应用程序的控制和扩展。
  • Hook 包括
    • 函数 Hook
    • 类 Hook
  • 常见的 Hook 技术 - Xposed - Frida - DexHunte

(5)Frida 简介

  • Frida 是一款基于 JavaScript 的动态分析工具
  • 主要特点:
    • 动态分析。
    • 脚本化。
    • 透明性。
    • 多平台支持。
    • 易于使用。

(6) 使用 Frida 进行代码 hook技术

需求:对litemall.apk的登录方法进行hook篡改登录账号和密码

环境准备。

  1. 初始化 node 项目:任意位置新建一个文件夹(不要影响其他代码),使用xcode打开文件键,输入以下命令创建一个node项目,会自动生成一个package.json文件;
npm init -y

  1. 准备被测的 APK 文件并安装到手机或者模拟器:下载链接
  2. Hook 目标:使用Android studio找到LoginActivity.java文件的loginMethod方法:

下载安装 Frida。

  1. 安装客户端:npm install frida

  2. 查看本地安卓版本:adb shell getprop ro.product.cpu.abi
    image

  3. 安装安卓版本对应的服务端:下载地址

启动 Frida。

  1. 解压到任意目录。

  2. 通过命令,把文件推到手机目录下:adb push frida-server-16.0.19-android-x86 /data/local/tmp

  3. 通过命令授权并启动 Frida。

# 进入设备
adb shell
# 进入frida所在目录
cd /data/local/tmp &&ls
# 修改frida权限
chmod +x frida-server-16.0.19-android-x86
# 启动Frida
./frida-server-16.0.19-android-x86

编写 Hook 脚本:在vscode中新建一个js文件(比如hook.js)。

  • 步骤:
    • 连接设备。
    • 获取目标应用程序的进程名称。
    • 连接应用进程。
    • 执行 hook 函数。
// 导入 Frida 模块
const frida = require("frida");
const util = require("util");
const exec = util.promisify(require("child_process").exec);

// 定义一个异步函数去监听app
async function attachToApp() {
  // 连接设备--使用await是为了让每一个命令执行后再执行下一个,不然所有的命令会同时执行,无法串行
  const device = await frida.getUsbDevice();
  console.log("device", device);
  // 获取目标应用程序的进程名称--exec方法用于执行adb命令
  const { stdout, stderr } = await exec(
    "adb shell ps | findstr com.ceshiren.litemall"
  );
  // adb命令执行后得到一个字符串,通过空格分割,分割后内容为真并且不是空格,获取第二个值
  const processName = stdout.split(" ").filter((s) => s && s.trim())[1];
  console.log("stdout", stdout.split(" ").filter((s) => s && s.trim())[1]);
  // 连接应用进程--将字符串转成数字
  const session = await device.attach(parseInt(processName));
  console.log("session", session);

  // 定义hook函数
  const script = await session.createScript(`
    Java.perform(function () {
      // 获取LoginActivity类
      let LoginActivity = Java.use('com.ceshiren.litemall.activity.LoginActivity');
      // 执行loginMethod方法
      let result1 = LoginActivity.loginMethod('', 'user123');
      console.log('result1', result1);
      let result2 = LoginActivity.loginMethod('user123', '');
      console.log('result2', result2);
      let result3 = LoginActivity.loginMethod('user123', 'user');
      console.log('result3', result3);
      let result4 = LoginActivity.loginMethod('user123', 'user123');
      console.log('result4', result4);
    });
  `);

  // 执行hook函数
  await script.load();
}

// 调用app监听函数
attachToApp();

运行 Hook 脚本。