原文链接:https://www.yuque.com/docs/share/35d28aa6-7754-4297-8fda-84df2fc16030?# 《Appium框架(hm)》

Part1 移动端测试环境搭建

学习目标

  1. 能够搭建java环境
  2. 能够搭建android环境

一、整体思路

我们的目标是Android测试,所以环境需要搭建三个,Java, AndroidSDK, Android模拟器。

为什么要安装这三个环境,我们倒着来说:

Android模拟器:实际上就是一台手机,方便我们给大家展示效果。

AndroidSDK:Android SDK给你提供开发测试所必须的Androld API类库。

Java:Androd的底层是C、C++,应用层用的语言是Java所以需要使用)ava环境。

二、环境搭建

1.1 Java环境

windows

安装JDK1.8

运行jdk- 8u151-windows-x64.exe文件,默认安装即可(例如我的安装目录: C:\Program Files\Java\jdk1.8.0)

配置java环境变量

验证环境变量

  1. win+r或者开始->搜索框输入cmd
  2. 在界面运行java -version
  3. 出现版本即可

1.2 AndroidSDK环境

安卓相关开发工具可以去官方开发者网站看,由于我写过一点安卓,这里先不详细记录了,可以访问下面的网站自己学习

https://developer.android.com/ (官网,需要网络工具)

https://developer.android.google.cn/ (中国官网,无需网络工具)

windows

将SDK保存到硬盘

  • Android SDK文件夹解压到任意目录(记住这个目录的位置,目录不要有中文

配置环境变量

  1. 进入我的电脑 -> 属性 -> 高级系统设置 -> 环境变量
  2. 在系统变量下点击新建 -> 变量名: ANDROID_HOME -> 变量值: D:\android-sdk ->点击确定按钮
  3. 在系统变量下找到系统的path变量,最后添加: ;%ANDROID_HOME%\platform-tools ;%ANDROID_ HOME%\tools;(最前面是一个分号,如果path变量最后已有分号,可不用添加) ->点击确定按钮

验证环境变量

  • 重启命令行工具,命令行输入adb,不报错即可

1.3 Android模拟器安装

略,我这里直接使用夜神模拟器

-----adb连接单个模拟器-----

1.连接模拟器

夜神模拟器,x86架构

adb connect 127.0.0.1:62001

网易MUMU模拟器

adb connect 127.0.0.1:7555

逍遥安卓模拟器

adb connect 127.0.0.1:21503

天天模拟器

adb connect 127.0.0.1:6555

海马玩模拟器

adb connect 127.0.0.1:53001

蓝叠模拟器,支持arm架构

adb connect 127.0.0.1:5555

2.断开模拟器

adb disconnect 127.0.0.1:62001

附加:无线adb

警告:使用Appium启动app请开启desired_caps["noReset"] = True ,否则将会清除app数据

desired_caps['dontStopAppOnReset'] = True # 不关闭应用
desired_caps['autoGrantPermissions'] = True # 自动获取权限
desired_caps["noReset"] = True # 不用每次重置

设置无线连接 (Android 10 及更低版本)

1、手机和电脑连入同一个无线网络

2、手机连接电脑,在命令行输入 adb tcpip 5555

3、断开连接线,命令行输入adb connect 10.3.6.59(手机的IP地址)

4、提示连接成功后,可以进行无线调试了

新手机设置 (Android 11 及更高版本)

  1. 如上所述,在设备上启用开发者选项。
  2. 如上所述,在设备上启用无线调试
  3. 在shell上,打开一个终端窗口并转到 android_sdk/platform-tools。
  4. 选择 Pair device with pairing code(使用配对码配对设备) ,找到您的 IP 地址、端口号和配对码。记下设备上显示的 IP 地址、端口号和配对代码。
  5. 在工作站的终端上,运行 adb pair ipaddr:port。请使用上述 IP 地址和端口号。
  6. 出现提示时,输入配对码,如下所示。

表明设备已成功配对的消息。

Part2 adb调试工具

2.1 概述

文档:

https://developer.android.com/studio/command-line/adb?hl=zh-cn

https://developer.android.google.cn/studio/command-line/adb?hl=zh-cn

学习目标:

  1. 能够了解adb的工作原理
  2. 能够应用常用的adb命令

2.2 adb的工作原理

2.2.1 adb的概念

ADB全名Android Debug Bridge,是一个调试工具。

  • 开发安卓应用的程序员必须要掌握
  • 测试工程师在做安卓应用测试时,会使用到

2.2.2 adb的构成和工作原理

提示:只需要知道组成部分和工作原理,会使用即可,面试可能会被问到

2.2.3 adb包含三个部分

  1. Client端 :运行在开发机器中,即你的开发电脑,用来发送adb命令;
  2. Daemon守护进程 :运行在调试设备中,手机或模拟器,用来接收并执行adb命令;
  3. Server端 :同样运行在开发机器中,用来管理Client端和手机的Daemon之间的通信。

2.2.4 adb工作原理

  1. client端将命令发送给server端
  2. server端会 将命令发送给daemon端
  3. doemon端进行执行
  4. 将执行结果,返回给server端
  5. server端将结果再返回给client端

小结

adb工具可以在电脑通过终端命令操作安卓手机/模拟器。

2.3 adb常用命令

2.3.1 获取包名和界面名【应用】

  1. 包名和界面名的概念
  2. 获取包名和界面名

2.3.1 包名和界面名的概念

  1. 包名(package):决定程序的唯一性(不是应用的名字)
  2. 界面名(activity) :目前可以理解,一个界面名,对应着一个界面。

2.1.2 获取包名和界面名

应用场景

自动化测试需要通过代码的形式告诉手机测试哪个应用程序的哪一个界面 ,所以需要通过这个命令进行获取。

使用步骤

  1. 打开需要测试的应用程序
  2. 输入adb命令

命令格式

  • Mac/Linux:

    • adb shell dumpsys window | grep mFocusedApp
  • Windows:

    • adb shell dumpsys window | findstr mFocusedApp

示例

作用:获取设置程序的包名和界面名

  1. 先在模拟器或手机中打开《设置》应用程序
  2. 输入对应平台的命令

结果如下:

mFocusedApp=ActivityRecord{26b8d02 u0 com.android.settings/.MainSettings t1984}

2.4 文件传输【应用】

2.4.1 发送文件到手机

  1. 从手机中拉取文件
  2. 发送文件到手机

应用场景

将手机需要的数据(数据库文件)在电脑上调整好,直接发送给手机

命令格式

adb push 电脑的文件路径 手机的文件夹路径

示例

作用: 将桌面的a.txt发送到手机的sd卡

adb push C:\users\hm\Desktop\a.txt /sdcard

2.4.2 从手机中拉取文件

应用场景

将手机产生的文件(数据库文件,日志文件)拉取到电脑中

命令格式

adb pull 手机的文件路径 电脑的文件夹路径

示例

作用:将手机的sd卡的a.txt拉取到桌面

adb pull /sdcard/a.txt C:\users\hm\Desktop

2.5 获取app启动时间[应用]

应用场景

  1. 如果企业对应用程序的启动速度有要求,则需要使用这个命令进行测试
  2. 测试标准:参照同类软件,启动时间不能超出一倍即可

命令格式

adb shell am start -W 包名/启动名

示例

作用: 启动com.android.settings程序并且进入主界面( .Settings )

adb shell am start -W com.android.settings/.MainSettings

Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.android.settings/.MainSettings }

Status: ok

LaunchState: WARM

Activity: com.android.settings/.MiuiSettings

TotalTime: 149

WaitTime: 152

Complete

解释

  1. ThisTime:该界面( activity )启动耗时(毫秒)
  2. TotalTime:应用自身启动耗时= ThisTime+ 应用application等资源启动时间(毫秒)
  3. WaitTime:系统启动应用耗时 = TotalTime+ 系统资源启动时间(毫秒)

2.6 获取手机日志[应用]

应用场景

将bug的日志信息发送给开发人员,便于开发人员定位bug

使用步骤

  1. 打开需要测试的应用程序
  2. 找到触发bug的位置
  3. 使用查看日志命令
  4. 触发bug
  5. 获取日志信息

命令格式

adb logcat

示例

  1. 安装bug.apk
  2. 打开《有bug的程序》应用程序
  3. 命令行中输入adb logcat命令
  4. 点击登录按钮
  5. 获取日志信息

2.7 其他命令【了解】

序号命令 说明
01adb install 路径/xx.apk安装app到手机
02adb uninstall 包名卸载手机上的app,需要指定包名
03adb devices获取当前电脑已经连接设备和对应的设备号
04adb shell进入到安卓手机内部的linux系统命令行中
05adb start-server启动adb服务端,出bug时使用可以重启服务器,先关闭再启动
06adb kill-server停止adb服务端,出bug时使用可以重启服务器,先关闭再启动
07adb --help查看adb帮助,命令记不清楚时有用

Part3 Appium自动化测试框架

3.1 概述

学习目标

  1. 能够安装Applum桌面客户端
  2. 能够安装Appium-python库

3.1.1 Appium介绍

Applum是一个移动端的自动化框架,可用于测试原生应用,移动网页应用和混合型应用,且是跨平台的。可用于iOS和Android操作系统。原生的应用是指用androld或iOS的sdk编写的应用,移动网页应用是指网页应用,类似于iOS中safari应用或者Chrome应用或者内浏览器的应用。混合应用是指一种包表webview的应用,原生应用于网页内容交互性的应用。

重要的是Appium是跨平台的,何为跨平台,意思就是可以针对不同的平台用一套api来编写测试脚本。

3.1.2 Appium自动化测试环境搭建

我们使用Appium和python来进行自动化测试,需要安装两个东西,一个是Appium的客户端,一个是Appium-python库。这两个需要安装的东西在加上手机就可以进行自动化测试,它们之间的关系是: python代码->Appium->python库-> Appium->手机。

Appium桌面客户端安装方式

  1. 运行appium-desktop-Setup-1.6.2.exe, 默认安装即可
  2. 启动客户端,按图片步骤1->2->3->4设置

image.png

  1. 启动成功展示如下图

Appium-python库安装

命令行安装(需要联网)

pip3 install Appium-Python-Client

可能出现的问题:

错误提示: could not create Wusr/oca/lb/python2.7/dist-packages/virtualenv. support: Permission denied

权限问题,使用管理员运行cmd

3.2 Hello Appium

学习目标:

  1. 能够使用appum启动任意应用程序
  2. 能够了解“前置代码”中各项参数的含义

3.2.1 快速体验

警告:启动app将会清除app数据,切勿使用常用机调试

应用场景

在做app自动化的时候,我们肯定是针对某个产品、某个软件进行测试,那么我们一定是先让模拟器或真机帮我们打开这款软件才可以。所以接下来要学的就是如何打开某个应用程序。

需求

使用以下步骤可以打开模拟器中的《设置》应用程序

步骤

由于是快速体验, 我们先进行复制粘贴代码看到效果后,再进行讲解

  1. 打开手机模拟器
  2. 打开appium工具
  3. 创建一个python项目,取名为hello_applum
  4. 创建一个demo.py文件
  5. 将下面代码直接复制,并运行即可
import time

from appium import webdriver

desired_caps = dict()
desired_caps['platformName'] = 'Android'
desired_caps['platformVersion'] = '5.1.1'
# 设备的名字,Android随意,非空;iOS有要求
desired_caps['deviceName'] = 'Nox'
desired_caps['appPackage'] = 'com.android.settings'
desired_caps['appActivity'] = '.Settings'

driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
time.sleep(5)

driver.quit()

注意点:

这段代码实际上配置了一些启动应用程序的相关参数。之后的其他项目也需要用到这个参数,可能有些参数配置的内容不同。为了方便我们后期课程会将这段代码叫做“前置代码”

appium启动部分手机报错

警告:未设置desired_caps["noReset"] ,启动app将会清除app数据

desired_caps['dontStopAppOnReset'] = True # 不关闭应用
desired_caps['autoGrantPermissions'] = True # 自动获取权限
desired_caps["noReset"] = True # 不用每次重置

报错内容:

java.lang.SecurityException: Permission denial: writing to settings requires:android.permission.WRITE_SECURE_SETTINGS

解决方案:

小米:在开发者选项里,把“USB调试(安全设置)”打开即可。 允许USB调试修改权限或模拟点击

oppo:在开发者选项里,把"禁止权限监控"打开即可。

Command output: adb: failed to install C:\Program Files\Appium Server GUI\resources\app\node_modules\appium\node_modules\io.appium.settings\apks\settings_apk-debug.apk: Failure [INSTALL_FAILED_USER_RESTRICTED: Install canceled by user]

小米:在开发者选项里,把“USB安装”打开即可。

3.2.2 参数详解【掌握】

应用场景

如果后期项目不是测试《设置》应用程序,而是测试《短信》应用程序那么怎么打开《短信》应用程序呢?如果后期项目测试的模拟器或手机不再是5.1的版本,而是6.1的版本呢?相关配置的信息在学习之后都可以进行修改。

参数解释

import time

from appium import webdriver

desired_caps = dict()
desired_caps['platformName'] = 'Android'
desired_caps['platformVersion'] = '5.1.1'
# 设备的名字,Android随意,非空;iOS有要求
desired_caps['deviceName'] = 'Nox'
desired_caps['appPackage'] = 'com.android.settings'
desired_caps['appActivity'] = '.Settings'

driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
time.sleep(5)

driver.quit()

启动过程【了解】

appium的启动实际上是在本机使用了4723端口开启了 一个服务

  1. 我们写的python代码会访间本机的appium服务器,井获取driver对象
  2. appium 会将我们的driver对象调用的方法转化成post请求,提交给appium服务器
  3. appium通过接收到的post请求发送给手机,再由手机进行执行

3.3 Appium基础操作API

学习目标

  1. 能够使用appium在脚本内启动其他app
  2. 能够使用appium获取包名和界面名
  3. 能够使用appium关闭app和驱动对象
  4. 能够使用appium安装和卸载app
  5. 能够使用appium将应用置于后台

前置代码

我这里用到了unittest,不清楚的可以自己去学下

import time
import unittest

from appium import webdriver


class BaseApi(unittest.TestCase):
    driver = None

    @classmethod
    def setUpClass(cls) -> None:
        desired_caps = dict()
        desired_caps['platformName'] = 'Android'
        desired_caps['platformVersion'] = '5.1.1'
        # 设备的名字,Android随意,非空;iOS有要求
        desired_caps['deviceName'] = 'Nox'
        # 如果填了下面两个参数,就会直接启动该app
        # desired_caps['appPackage'] = 'com.android.settings'
        # desired_caps['appActivity'] = '.Settings'

        cls.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)

    @classmethod
    def tearDownClass(cls) -> None:
        cls.driver.quit()

3.3.1 在脚本内启动其他app

应用场景

如果一个应用需要跳转到另外一个应用,就可以使用这个api进行应用的跳转,就像我们通过外卖应用下订单之后会跳转到支付应用一样。

方法名和参数

# 脚本内启动其他app
# 参数:
#     appPackage: 要打开的程序的包名
#     appActivity: 要打开的程序的界面名
driver.start_activity(appPackage, appactivity)

示例

def test1(self):
    """
    打开设置3秒后跳转下载
    """
    self.driver.start_activity('com.android.settings', '.Settings')
    time.sleep(3)
    self.driver.start_activity('com.android.documentsui', '.DocumentsActivity')
    time.sleep(2)

3.3.2 获取app的包名和界面名

应用场景

当我们从一个应用跳转到另外一个应用的时候,想输出其包名、界面名或者想在报告中展现对应信息,我们就可以调用这个属性来进行获取

属性名

# 安卓获取包名
driver.current_package
#获取界面名
driver.current_activity

示例

def test2(self):
    """
    获取app的包名和界面名
    """
    print("当前包名" + self.driver.current_package)
    print("当前界面名" + self.driver.current_activity)

输出:

当前包名com.android.settings

当前界面名.Settings

3.3.3 关闭app和驱动对象

应用场景

有的时候我们需要关闭某个应用程序后,再打开新的应用。那么如何关闭应用程序呢?

方法名

# 关闭当前操作的app,不会关闭驱动对象
driver.close_app()
# 关闭驱动对象,同时关闭所有关联的app
driver.quit()

示例

打开 《设置》,使用close_app()方法关闭,再尝试使用quit()方法,最后打印当前程序的包名,观察区别

def test3(self):
    """
    打开 《设置》,使用close_app()方法关闭,
    再尝试使用quit()方法,最后打印当前程序的包名,观察区别
    """
    self.driver.start_activity('com.android.settings', '.Settings')
    self.driver.close_app()
    print("当前包名" + self.driver.current_package)

    time.sleep(2)

    self.driver.start_activity('com.android.settings', '.Settings')
    self.driver.quit()
    print("当前包名" + self.driver.current_package)

输出:

当前包名com.vphone.launcher

后面报错

NoSuchDriverError: A session is either terminated or not started

...

3.3.4 安装和卸载以及是否安装app

应用场景

一些应用市场的软件可能会有一个按钮,如果某一个程序已经安装则卸载, 如果没有安装则安装

方法名

# 安装app
driver.is_app_installed("mark.via")
# 卸载app
self.driver.remove_app("mark.via")
# 判断app是否已经安装;返回值为布尔
self.driver.is_app_installed("mark.via")

示例

如果《Via》已经安装,则卸载《Via》,如果没有则安装

def test4(self):
    """
    如果《Via》已经安装,则卸载《Via》,如果没有则安装
    """
    # 判断安智市场是否已经安装
    if self.driver.is_app_installed("mark.via"):
        self.driver.remove_app("mark.via")
    else:
        path = os.getcwd() + "/file/mark.via.apk"
        print("apk文件位置:" + path)
        self.driver.install_app(path)

3.3.5 将应用置于后台

应用场景

银行类app会在进入后台一定时间后,如果再回到前台也页面会重新输入密码,如果需要自动化测试这种功能,可以使用这个api进行测试

方法

# app放置到后台一定时间后再回到前台,模拟热启动
# 参数:
# seconds: 后台停留多少秒
driver.background_app(seconds)

示例

def test5(self):
    """
    app放置到后台一定时间后再回到前台,模拟热启动
    """
    self.driver.start_activity('com.android.settings', '.Settings')
    print("----准备进入后台----")
    self.driver.background_app(5)
    print("----已经回到前台----")

注意

这个方法会自动回到前台

热启动:表示进入后台回到前台。关机再开这种切断电源的行为可以叫做“冷启动”

3.2 UI Automator Viewer

学习目标:

  1. 能够使用UlAutomatorViewer获取元素的特征信息

应用场景

定位元素的时候必须根据元素的相关特征来进行定位,而UIAutomatorViewer就是用来获取元素特征的。

简介

UIAutomatorViewer用来扫描和分析Android应用程序的UI控件的工具。

使用步骤

  1. 进入SDK目录下的目录

    1. mac在tools/bin目录下,打开uiautomatorviewer
    2. windows在AndroidSDK\tools\bin目录下,打开uiautomatorviewer.bat
  2. 电脑连接真机或打开android模拟器
  3. 启动待测试app
  4. 点击uiautomatorviewer的左上角Device Screenshot (从左数第二 个按钮)
  5. 点击希望查看的控件
  6. 查看右下角Node Detail相关信息

示例

查看《设置》应用程序右上角“放大镜"按钮的"resource-id"

  1. 打开uiautomatorviewer
  2. 打开android模拟器
  3. 启动《设置》应用程序
  4. 点击Device Screenshot按钮
  5. 点击“放大镜”按钮!
  6. 查看Node Detal中的"resource-ld"信息

注意点

  1. 自动打开的命令行窗口不要关
  • 如果关了,整个工具也会关闭
  1. 打开uiautomatorviewer闪退
  • 解决方案:jdk版本问题造成的,jdk为1.9时 可能会出现这个问题,请换成1.8的版本
  1. 点击第二个按钮报错
  • 解决方案:

    • adb kill-server
    • adb start-server

3.3 元素定位操作API

学习目标

  1. 能够分别使用id.class.xpath定位某一个元素
  2. 能够分别使用id.class.xpath 定位某一组元素

应用场景

计算机不像人一样聪明,我们需要通过元素定位来获取元素,才能让计算机帮我们“操作”这个元素。

步骤

  1. 打开uiautomatorviewer工具
  2. 打开模拟器或真机
  3. 通过uiautomatorviewer工具获取想要进行操作的元素的Node Detail信息
  4. 通过元素定位API进行定位
  5. 对元素进行相关操作

注意点

元素的定位基于当前屏幕范围内展示的可见元素。

3.3.1 定位一个元素【掌握】

应用场景

想要对按钮进行点击,想要对输入框进行输入。想要获取文本框的内容,定位元素是自动化操作必须要使用的方法。只有获取元素之后,才能对这个元素进行操作。

方法名

定位ID:

driver.find_element(By.ID, ...)

定位class name:

driver.find_element(By.CLASS_NAME, ...)

定位xpath:

driver.find_element(By.XPATH, ...)

示例

通过id的形式,定位“放大镜”按钮,并点击

通过class的形式,定位“输入框”,输入“hello”

通过xpath的形式,定位“返回”按钮。并点击

关键代码

def test_find_element(self):
    """
    通过id的形式,定位“放大镜”按钮,并点击
    通过class的形式,定位“输入框”,输入“wlan”
    通过xpath的形式,定位“返回”按钮。并点击
    """
    self.driver.start_activity('com.android.settings', '.Settings')
    self.driver.find_element(By.ID, "com.android.settings:id/search").click()
    sleep(2)
    self.driver.find_element(By.CLASS_NAME, "android.widget.EditText").send_keys('wlan')
    sleep(2)
    self.driver.find_element(By.XPATH, "//*[@content-desc='收起']").click()

附加-xpath定位工具

https://github.com/yaming116/uiautomatorview

使用方式:

方式1.直接下载根目录下的uiautomatorviewer.jar,替换你本地的${ANDROID_HOME}/tools/lib下的uiautomatorviewer.jar即可

方式2.执行gradle jar命令(或gradlew jar),编译工程,将build目录下编译出来的uiautomatorviewer.jar替换你本地的${ANDROID_HOME}/tools/lib下的uiautomatorviewer.jar即可

我的使用方式

  1. 下载上面提到的uiautomatorviewer.jar放到${ANDROID_HOME}/tools/lib下,并重命名为uiautomatorviewer-xpath.jar
  2. ${ANDROID_HOME}/tools/bin下复制uiautomatorviewer.bat文件,修改其中的uiautomatorviewer-26.0.0-dev为uiautomatorviewer-xpath.jar

    @echo off
    rem Copyright (C) 2012 The Android Open Source Project
    rem
    rem Licensed under the Apache License, Version 2.0 (the "License");
    rem you may not use this file except in compliance with the License.
    rem You may obtain a copy of the License at
    rem
    rem      http://www.apache.org/licenses/LICENSE-2.0
    rem
    rem Unless required by applicable law or agreed to in writing, software
    rem distributed under the License is distributed on an "AS IS" BASIS,
    rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    rem See the License for the specific language governing permissions and
    rem limitations under the License.
    
    rem don't modify the caller's environment
    setlocal
    
    rem Set up prog to be the path of this script, including following symlinks,
    rem and set up progdir to be the fully-qualified pathname of its directory.
    set prog=%~f0
    
    rem Change current directory and drive to where the script is, to avoid
    rem issues with directories containing whitespaces.
    cd /d %~dp0
    
    rem Get the CWD as a full path with short names only (without spaces)
    for %%i in ("%cd%\..") do set prog_dir=%%~fsi
    
    rem Check we have a valid Java.exe in the path.
    set java_exe=
    call ..\lib\find_java.bat
    if not defined java_exe goto :EOF
    
    for /f %%a in ("%APP_HOME%\lib\uiautomatorviewer-xpath.jar") do set jarfile=%%~nxa
    set frameworkdir=.
    
    if exist %frameworkdir%\%jarfile% goto JarFileOk
    set frameworkdir=..\lib
    
    if exist %frameworkdir%\%jarfile% goto JarFileOk
    set frameworkdir=..\framework
    
    :JarFileOk
    
    set jarpath=%frameworkdir%\%jarfile%
    
    if not defined ANDROID_SWT goto QueryArch
    set swt_path=%ANDROID_SWT%
    goto SwtDone
    
    :QueryArch
    
    for /f "delims=" %%a in ('%frameworkdir%\..\bin\archquery') do set swt_path=%frameworkdir%\%%a
    
    :SwtDone
    
    if exist "%swt_path%" goto SetPath
    echo SWT folder '%swt_path%' does not exist.
    echo Please set ANDROID_SWT to point to the folder containing swt.jar for your platform.
    exit /B
    
    :SetPath
    set javaextdirs=%swt_path%;%frameworkdir%
    
    call "%java_exe%" "-Djava.ext.dirs=%javaextdirs%" "-Dcom.android.uiautomator.bindir=%prog_dir%" -jar %jarpath% %*

api26测试无效

3.3.2 定位一组元素【掌握】

应用场景

和定位一个元素相同,但如果想要批量的获取某个相同特征的元素,使用定位一组元素的方式更加方便。

方法名

定位ID:

driver.find_elements(By.ID, ...)

定位class name:

driver.find_elements(By.CLASS_NAME, ...)

定位xpath:

driver.find_elements(By.XPATH, ...)

示例

通过id的形式,获取所有resourceid为"com.android.settings:id/title"的元素,并打印其文字内容

通过class_name的形式,获取所有class 为"android.widget.TextView"的元素,并打印其文字内容

通过xpath的形式,获取所有包含“设”的元素,并打印其文字内容

关键代码

def test_find_elements(self):
    """
    通过id的形式,获取所有resourceid为"com.android.settings:id/title"的元素,并打印其文字内容
    通过class_name的形式,获取所有class为"android.widget.TextView"的元素,并打印其文字内容
    通过xpath的形式,获取所有包含"设"的元素,并打印其文字内容
    """
    self.driver.start_activity('com.android.settings', '.Settings')
    titles = self.driver.find_elements(By.ID, "com.android.settings:id/title")
    print("===========所有title元素的文字内容:===========")
    for title in titles:
        print(title.text)
    text_views = self.driver.find_elements(By.CLASS_NAME, "android.widget.TextView")
    print("===========所有text_view元素的文字内容:===========")
    for view in text_views:
        print(view.text)
    elements = self.driver.find_elements(By.XPATH, "//*[contains(@text,'设')]")
    print("===========所有包含“设”的元素:===========")
    for element in elements:
        print(element.text)

3.3.3 定位元素的注意点【了解】

应用场景

了解这些注意点可以以后在出错误的时候,更快速的定位问题原因。

示例

使用 find_element_by_xx成find_elements_by_xx的方法,分别传入一个没有的“特征”会是什么结果呢?

小结

  1. 如果使用find_element_by_xx方法,如果传入一个没有的特征,会报NoSuchElementException的错误。
  2. 如果使用find_elements_by_xx方法,如果传入一个没有的特征,不会报错,会返回一个空列表

3.4 元素等待

学习目标

  1. 能够使用隐式等待来定位元素
  2. 能够使用显式等待来定位元素

3.4.1 元素等待

应用场景

可能由于一些原因,我们想找的元素并没有立刻出来,此时如果直接定位可能会报错,比如以下原因:

  1. 由于网络速度原因
  2. 服务器处理请求原因
  3. 电脑配置原因

概念

WebDriver定位页面元素时如果未找到,会在指定时间内一直等待的过程

元素等待一共分为两种类型

  1. 显式等待
  2. 隐式等待

3.4.2 隐式等待

应用场景

针对所有定位元素的超时时间设置为同一个值的时候

概念

等待元素加载指定的时长,超出时长抛出NoSuchElementException异常

步骤

  1. 在获取driver对象后,使用driver调用implicity.wait方法即可

示例

在5秒钟内, 在《设置》程序中的“返回”按钮,如果找到则点击。如果找不到则观察对应错误信息。

核心代码

def test_implicitly_wait(self):
    """
    在5秒钟内,在《设置》程序中的“返回”按钮,如果找到则点击。如果找不到则观察对应错误信息。
    """
    self.driver.implicitly_wait(5)
    self.driver.find_element(By.XPATH, "//*[@content-desc='收起']").click()

3.4.3 显式等待

应用场景

针对所有定位元素的超时时间设置为不同的值的时候

概念

等待元素加载指定的时长,超出时长抛出TimeoutException异常

步骤

  1. 导包
  2. 创建WebDriverWait对象
  3. 调用WebDriverWait对象的until方法

示例

在5秒钟内, 每1秒(默认0.5秒)在《设置》程序中的“返回”按钮,如果找到则点击。如果找不到则观察对应错误信息。

核心代码

def test_wait(self):
    """
    在5秒钟内,每1秒(默认0.5秒)在《设置》程序中的“返回”按钮,如果找到则点击。如果找不到则观察对应错误信息。
    """
    wait = WebDriverWait(self.driver, 5, 1)
    wait.until(lambda x: x.find_element(By.XPATH, "//*[@content-desc='收起']"))
    self.driver.find_element(By.XPATH, "//*[@content-desc='收起']").click()

3.4.4 隐式等待和显式等待的选择

  • 作用域:

    • 显式等待为单个元素有效,隐式为全局元素
  • 方法:

    • 显式等待方法封装在WebDriverWait类中,而隐式等待则直接通过driver实例化对象调用
  • 关于sleep的形式?

    • sleep 是固定死一个时间,不是不行,是不推荐。
    • 元素等待可以让元素出来的第一时间进行操作。sleep可能造成不必要的浪费。

3.5 元素操作API

学习目标

  1. 能够使用代码点击按钮
  2. 能够使用代码对输入框输入文字
  3. 能够使用代码对输入框清空文字
  4. 能够使用代码获取元素的文本内容
  5. 能够使用代码获取元素的位置和大小
  6. 能够使用代码根据属性名获取元素的属性值

3.5.1 点击元素

应用场景

需要点击某个按钮的时候使用

方法名

对element按钮进行点击操作

element.click()

示例

  1. 打开《设置》
  2. 点击放大镜按钮

核心代码

self.driver.find_element(By.ID, "com.android.settings:id/search").click()

3.5.2 输入和清空输入框内容

应用场景

需要对输入框进行输入或清空的时候使用

方法名

对element输入框进行输入操作

element.send_keys('wlan')

对element输入框进行清空操作

element.clear()

示例

  1. 打开《设置》
  2. 点击“放大镜”
  3. 输入"hello"
  4. 暂停2秒
  5. 清空所有文本内容
  6. 暂停5秒
  7. 输入“你好”
def test_input_and_clear(self):
    # 这里多添加了隐式等待,因为我这里不添加等待,点击放大镜会找不到
    self.driver.implicitly_wait(5)
    self.driver.start_activity('com.android.settings', '.Settings')
    self.driver.find_element(By.ID, "com.android.settings:id/search").click()
    element = self.driver.find_element(By.CLASS_NAME, "android.widget.EditText")
    element.send_keys('hello')
    sleep(2)
    element.clear()
    sleep(5)
    element.send_keys('你好')

注意点

若输入中文无效, 但不会报错,需要在“前置代码”中增加两个参数

desired_caps['unicodeKeyboard'] = True

desired_caps['resetKeyboard'] = True

3.5.3 获取元素的文本内容

应用场景

需要获取按钮、文本框、输入框等控件的文本内容时使用

属性名

element.text

示例

  1. 打开《设置》
  2. 获取所有获取resource-id为"com.android.settings:id/title"的元素,并打印其文字内容

核心代码

def test_get_text(self):
    """
    获取所有resource-id为"com.android.settings:id/title"的元素,并打印其文字内容
    """
    elements = self.driver.find_elements(By.ID, "com.android.settings:id/title")
    for element in elements:
        print(element.text)

3.5.4 获取元素的位置和大小

应用场景

需要获取元素的位置和大小的时候使用

属性名

search.location

search.size

示例

  1. 打开《设置》
  2. 获取“放大镜”的位置和大小

核心代码

def test_get_location(self):
    """
    获取“放大镜”的位置和大小
    """
    search = self.driver.find_element(By.ID, "com.android.settings:id/search")
    print(search.location)
    print(search.size)

输出

{'x': 804, 'y': 58}

{'height': 96, 'width': 96}

3.5.5 获取元素的属性值

应用场景

根据特征定位到元素后,使元素的属性名获取对应的属性值

方法名

title.get_attribute("text")

示例

  1. 打开《设置》
  2. 获取所有获取resource-id为"com.android.settings:id/title"的元素
  3. 使用get_attribute获取这些元素的text,enabled,content-desc,resource-id,class的属性值

核心代码

def test_get_attribute(self):
    """
    获取所有获取resource-id为"com.android.settings:id/title"的元素
    使用get_attribute获取这些元素的text,enabled,content-desc,resource-id,class的属性值
    """
    titles = self.driver.find_elements(By.ID, "com.android.settings:id/title")
    for title in titles:
        print(title.get_attribute("text"))
        print(title.get_attribute("enabled"))
        print(title.get_attribute("content-desc"))
        print(title.get_attribute("resource-id"))
        print(title.get_attribute("class"))
        print("================")

注意点

在旧版本中:

value='text'返回text的属性值

value='name'返回content-desc/text属性值

value='className'返回class属性值,只有API=>18才能支持

value='resourceld'返回resource-id属性值,只有APlI=>18 才能支持

3.6 滑动和拖拽事件

学习目标

  1. 能够使用swipe滑动屏幕
  2. 能够使用scroll滑动屏幕
  3. 能够使用drag_and_drop滑动屏幕

3.6.1 滑动和拖拽事件

应用场景

我们在做自动化测试的时候,有些按钮是需要滑动几次屏幕后才会出现,此时,我们需要使用代码来模拟手指的滑动,也就是我们将要学习的滑动和拖拽事件

3.6.2 swipe滑动事件

概念

从一个坐标位置滑动到另一个坐标位置,只能是两个点之间的滑动。

方法名

driver.swipe(100, 1000, 100, 100, 800)

driver.swipe(起始x坐标, 起始y坐标, 结束x坐标, 结束y坐标, [持续时间])

示例1

模拟手指从(100, 1000),滑动到(100, 800)的位置

def test1(self):
    """
    模拟手指从(100, 1000),滑动到(100, 800)的位置
    """
    self.driver.implicitly_wait(5)
    self.driver.swipe(100, 1000, 100, 100, 800)

示例2

模拟手指从(100, 1000),滑动到(100, 500)的位置

def test2(self):
    """
    模拟手指从(100, 1000),滑动到(100, 500)的位置
    """
    self.driver.implicitly_wait(5)
    self.driver.swipe(100, 1000, 100, 100, 500)

小结

距离相同时,持续时间碰长,惯性越小

持续时间相同时,手指滑动的距离越大,实际滑动的距离也就越大

注意点

注意屏幕分辨率,超出分辨率的坐标可能出现Unable to perform W3C actions</span><span class="ne-text">. Check the logcat output for possible error reports and make sure your input actions chain is valid.的问题

3.6.3 scroll滑动事件

概念

从一个元素滑动到另一个元素,直到页面自动停止。

方法名

driver.scroll(origin_el, destination_el)

driver.scroll(滑动开始的元素, 滑动结束的元素, [持续时间])

示例

从“存储”滑动到“更多”

核心代码

def test_scroll(self):
    """
    从“存储”滑动到“更多”
    """
    self.driver.implicitly_wait(5)
    e1 = self.driver.find_element(By.XPATH, "//*[@text='存储']")
    e2 = self.driver.find_element(By.XPATH, "//*[@text='更多']")
    self.driver.scroll(e1, e2)

小结

能设置持续时间,惯性很大

3.6.4 drag_and_drop拖拽事件

概念

从一个元素滑动到另一个元素, 第二个元素替代第一个元素原本屏幕上的位置。

方法名

driver.drag_and_drop(origin_el, destination_el)

driver.drag_and_drop(滑动开始的元素, 滑动结束的元素)

示例

将 “存储”拖拽到“更多”

核心代码

def test_drag_and_drop(self):
    """
    从“存储”滑动到“更多”
    """
    self.driver.implicitly_wait(5)
    e1 = self.driver.find_element(By.XPATH, "//*[@text='存储']")
    e2 = self.driver.find_element(By.XPATH, "//*[@text='更多']")

    self.driver.drag_and_drop(e1, e2)

小结

不能设置持续时间, 没有惯性

3.6.5 滑动和拖拽事件的选择

滑动和拖拽无非就是考虑是否有“惯性”, 以及传递的参数是“元素”还是“坐标”。

可以分成以下四种情况

  • 有“惯性”,传入“元素”

    • scroll
  • 无“惯性”,传入“元素”

    • drag_and_drop
  • 有“惯性”,传入“坐标”

    • swipe,并且设置较短的duration时间
  • 无“惯性”,传入“坐标”

    • swipe,并且设置较长的duration时间

3.7 高级手势TouchAction

于2.0弃用,可使用 W3C actions 代替(ActionChains),但部分功能缺失,具体讨论可看: https://github.com/appium/appium/issues/16009

*deprecated:: 2.0.0
* Please use W3C actions instead: http://appium.io/docs/en/commands/interactions/actions/

学习目标

  1. 能够使用代码完成轻敲手势
  2. 能够使用代码完成按下手势
  3. 能够使用代码究成抬起手势
  4. 能够使用代码究成等待操作
  5. 能够使用代码究成长按手势
  6. 能够使用代码究成手指移动操作

应用场景

TouchAction 可以实现一些针对手势的操作,比如滑动、长按、拖动等,我们可以将这些基本手势组合成一个相对复杂的手势。比如,我们解锁手机或者-些应用软件都有手势解锁的这种方式。

使用步骤

  1. 创建TouchAction对象
  2. 通过对象调用想执行的手势
  3. 通过perform()执行动作

注意点

所有手势都要通过执行perform()函数才会运行。

3.6.1 手指轻敲操作【掌握】

应用场景

模拟手指对某个元素或坐标按下并快速抬起。比如,固定点击(100, 100)的位置

方法名

def tap(self,

element: WebElement | None = None,

x: int | None = None,

y: int | None = None,

count: int = 1) -> TouchAction
TouchAction(driver).tap(e1).perform()

示例

  1. 打开《设置》
  2. 轻敲"WLAN"

核心代码

def test1(self):
    """
    1. 打开《设置》 2. 轻敲"WLAN"
    """
    self.driver.start_activity('com.android.settings', '.Settings')
    e1 = self.driver.find_element(By.XPATH, '//*[@text="WLAN"]')
    TouchAction(self.driver).tap(e1).perform()

3.6.2 按下和抬起操作【掌握】

应用场景

模拟手指一直按下模拟手指抬起。可以用来组合成轻敲或长按的操作

方法名

按下

TouchAction(self.driver).press(x=650, y=330).perform()

抬起

TouchAction(self.driver).press(x=650, y=330).release().perform()

示例1

使用坐标的形式按下WLAN (650,650) ,2秒后,按下(650, 650)的位置

核心代码

def test2(self):
    """
    使用坐标的形式按下WLAN (650, 330) ,2秒后,按下(650, 330)的位置
    """
    self.driver.start_activity('com.android.settings', '.Settings')
    TouchAction(self.driver).press(x=650, y=330).perform()
    sleep(2)
    TouchAction(self.driver).press(x=650, y=330).perform()

示例2

使用坐标的形式点击WLAN (650,650) ,2秒后,按下(650, 650)的位置,并抬起

核心代码

def test3(self):
    """
    使用坐标的形式点击WLAN (650,330) ,2秒后,按下(650, 330)的位置,并抬起
    """
    self.driver.start_activity('com.android.settings', '.Settings')
    TouchAction(self.driver).press(x=650, y=330).perform()
    sleep(2)
    TouchAction(self.driver).press(x=650, y=330).release().perform()

3.6.3 等待操作【掌握】

应用场景

模拟手指等待,比如按下后等待5秒之后再抬起。

方法名

TouchAction(driver).wait(ms=0).perform()

示例

使用坐标的形式点击 WLAN (650,330),2秒后,按下(650, 330)的位置,暂停2秒, 并抬起

核心代码

def test4(self):
    """
    使用坐标的形式点击WLAN (650,330),2秒后,按下(650, 330)的位置,暂停2秒, 并抬起
    """
    self.driver.start_activity('com.android.settings', '.Settings')
    TouchAction(self.driver).press(x=650, y=330).perform()
    sleep(2)
    TouchAction(self.driver).press(x=650, y=330).wait(2000).release().perform()

3.6.4 长按操作【掌握】

应用场景

模抵手指对元素或坐标的长按操作。比如,长按某个按钮弹出菜单。

方法名

def long_press(
self ,
el: Optional['WebElement'] = None ,
x: Optional[int] = None ,
y: Optional[int] = None ,
duration: int = 1000,
) -> 'TouchAction':

示例

使用坐标的形式点击 WLAN (650,330),2秒后,长按(650, 330)的位置持续2秒

核心代码

def test5(self):
    """
    使用坐标的形式点击 WLAN (650,330),2秒后,长按(650, 330)的位置持续2秒
    """
    self.driver.start_activity('com.android.settings', '.Settings')
    TouchAction(self.driver).press(x=650, y=330).perform()
    sleep(2)
    TouchAction(self.driver).long_press(x=650, y=330).perform()

3.6.5 移动操作【掌握】

应用场景

模拟手指移动操作,比如,手势解锁需要先按下,再移动。

方法名

def move_to(
self , el: Optional['WebElement'] = None , x: Optional[int] = None , y: Optional[int] = *None

  • ) -> 'TouchAction':

示例

画一个Z字的案例

核心代码

def test6(self):
    """
    画一个Z字的案例
    """
    (TouchAction(self.driver).press(x=150, y=580).move_to(x=450, y=580)
     .move_to(x=750, y=580).move_to(x=450, y=880).move_to(x=150, y=1180)
     .move_to(x=450, y=1180).move_to(x=750, y=1180).release().perform())

3.7 W3C actions (ActionChains)

http://appium.io/docs/en/commands/interactions/actions/#description

TouchAction于2.0弃用,可使用 W3C actions 代替(ActionChains),但部分功能缺失,具体讨论可看: https://github.com/appium/appium/issues/16009

*deprecated:: 2.0.0
* Please use W3C actions instead: http://appium.io/docs/en/commands/interactions/actions/

3.7.1 前置代码

基本上和之前的一样需要导入from selenium.webdriver import ActionChains使用ActionChains

import unittest

from appium import webdriver
from selenium.webdriver import ActionChains


class ActionChainsApi(unittest.TestCase):
    driver = None

    @classmethod
    def setUpClass(cls) -> None:
        desired_caps = dict()
        desired_caps['platformName'] = 'Android'
        desired_caps['platformVersion'] = '5.1.1'
        # 设备的名字,Android随意,非空;iOS有要求
        desired_caps['deviceName'] = 'Nox'
        cls.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)

    @classmethod
    def tearDownClass(cls) -> None:
        cls.driver.quit()

3.7.2 重构项目

由于基于坐标的 W3C actions 尚未实现,这里仅写部分

import unittest

from appium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By

"""
高级手势TouchAction于2.0弃用,可使用 W3C actions 代替(ActionChains),
但部分功能缺失,具体讨论可看:https://github.com/appium/appium/issues/16009
"""


class ActionChainsApi(unittest.TestCase):
    driver = None

    @classmethod
    def setUpClass(cls) -> None:
        desired_caps = dict()
        desired_caps['platformName'] = 'Android'
        desired_caps['platformVersion'] = '5.1.1'
        # 设备的名字,Android随意,非空;iOS有要求
        desired_caps['deviceName'] = 'Nox'
        cls.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
        cls.action = ActionChains(cls.driver)

    def test1(self):
        """
        1. 打开《设置》 2. 轻敲"WLAN"
        """
        self.driver.start_activity('com.android.settings', '.Settings')
        e1 = self.driver.find_element(By.XPATH, '//*[@text="WLAN"]')
        ActionChains(self.driver).click(e1).perform()

    @classmethod
    def tearDownClass(cls) -> None:
        cls.driver.quit()

3.8 手机操作API

学习目标

  1. 能够获取手机分辨率
  2. 能够获取手机数图
  3. 能够获取和设置网络状态
  4. 能够发送键到设备
  5. 能够打开和关闭手机遇知栏

3.8.1 获取手机分辨率【掌握】

应用场景

自动化测试可能会需要根据当前设备的屏幕分解率来计算-些点击或者滑动的坐标

方法名

获取手机分辨率

driver.get_window_size()

示例

输出当前设备的屏幕分辨率

def test1(self):
    """
    输出当前设备的屏幕分辨率
    """
    print('分辨率:' + self.driver.get_window_size().__str__())
    print('width:' + self.driver.get_window_size()['width'].__str__())
    print('height:' + self.driver.get_window_size()['height'].__str__())

执行结果

分辨率:{'width': 900, 'height': 1600}

width:900

height:1600

3.8.2 手机截图【掌握】

应用场景

有些自动化的操作可能没有反应。但并不报错。此时我们就可以将操作过后的关键情况,截图留存。后期也可以根据图片发现问题。

方法名

获取手机分辨率

def get_screenshot_as_file(self , filename) -> bool

示例

  1. 打开设置页面
  2. 截图当前页面保存到当前目录,命名为screen.png
def test2(self):
    """
    打开设置页面
    截图当前页面保存到当前目录,命名为screen.png
    """
    self.driver.start_activity('com.android.settings', '.Settings')
    self.driver.get_screenshot_as_file(os.getcwd() + os.sep + './screen.png')

执行效果

项目目录下会将设置页面保存成screen.png

3.8.3 获取和设置手机网络【掌握】

应用场景

视频应用在使用流量看视频的时候,大部分都会提示用户正在是否继续播放。作为测试人员,我们可能需要用自动化的形式来判断是否有对应的提示。即,用流量的时候应该有提示,不用流量的时候应该没有提示。

获取手机网络

属性名

获取手机网络

driver.network_connection

示例

获取当前网络类型,并打印

核心代码

def test3(self):
    """
    获取手机网络
    """
    print(self.driver.network_connection)
Value (Alias) DataWifiAirplane Mode
0 (None)000
1 (Airplane Mode)001
2 (Wifi only)010
4 (Data only)100
6 (All network on)110

设置手机网络

方法名

设置手机网络

def set_network_connection(self , connection_type: int) -> int

示例

设置当前设备为飞行模式

def test4(self):
    """
    设置当前设备为飞行模式
    """
    print(self.driver.set_network_connection(ConnectionType.AIRPLANE_MODE))

核心代码

driver.set_network_connection(1)

执行效果

设备变为飞行模式

注意点

网络的类型, 建议使用系统提供的类型

from appium.webdriver.connectiontype import ConnectionType

3.8.4 发送键到设备【掌握】

应用场景

模拟按“返回键”"home键等等操作,比如,很多应用有按两次返回键退出应用的功能,如果这个功能需要我们做自动化,那么一定会用到这个方法

方法名*

  • 发送键到设备

driver.press_keycode(24)

注意点

按键对应的编码, 可以在百度搜索关键字"android keycode"

例如: https://www.cnblogs.com/xiaowenshu/p/10012794.html

示例

点击三次音量加, 再点击返回,再点击两次音量减。

def test5(self):
    """
    点击三次音量加,再点击返回,再点击两次音量减。
    """
    self.driver.press_keycode(24)
    sleep(1)
    self.driver.press_keycode(24)
    sleep(1)
    self.driver.press_keycode(24)
    sleep(1)
    self.driver.press_keycode(4)
    sleep(1)
    self.driver.press_keycode(25)
    sleep(1)
    self.driver.press_keycode(25)

3.8.5 操作手机通知栏【掌握】

应用场景

测试即时通信类软件的时候,如果A给B发送一条消息, B的通知栏肯是会显示对应的消息。我们想通过通知栏来判断B是否收到消息,一定要先操作手机的通知栏

方法名

打开手机通知栏

driver.open_notifications()

注意点

appium官方并没有为我们提供关闭通知的api,那么现实生活中怎么关闭,就怎样操作就行,比如,手指从下往,上滑动,或者,按返回键

示例

打开通知栏,两秒后,关闭通知栏

def test6(self):
    """
    打开通知栏,两秒后,关闭通知栏
    """
    self.driver.open_notifications()
    sleep(2)
    self.driver.swipe(100, 800, 100, 200, 500)
Last modification:April 9, 2022
如果觉得我的文章对你有用,请随意赞赏