测试流程
1、参与需求评审
2、编写测试计划和测试方案
a. 要测试的功能模块
b. 测试人员分工和时间安排
c. 测试环境
d. 测试策略
e. 风险评估
3. 编写测试用例并组织用例评审
4. 执行测试用例,跟踪缺陷解决
5. 编写测试报告
a. 邮件形式
b. 覆盖功能模块
c. 测试用例覆盖率
d. 缺陷攻击/遗留缺陷
e. 结论:是否允许上线
熟悉项目
- 项目的作用
- 项目的用户和角色
- 项目的功能架构:功能模块,功能点
测试要点
- 功能测试⭐️
- 兼容性测试⭐️
- 安装、卸载、升级测试⭐️
- 交叉事件测试⭐️
- PUSH测试⭐️
- 性能测试:CPU、内存、流量测试、电量测试、流畅度测试、启动测试
- 用户体验测试⭐️
- 稳定性测试
兼容性
云测平台 testin
统计网站:https://gs.statcounter.com/android-version-market-share/mobile-tablet/united-states-of-america
- 手机型号
- 系统版本:ios15/16/17/18(17和18最多);android 11/12/13/14/15(13和14最多)
- 屏幕尺寸、分辨率。
a. 分辨率:720 x 1280、1440 x 2560、1080 x 1920、2160 x 3840 - 网络:3G、4G、5G、WIFI
- 应用兼容性
a. 与手机硬件兼容:home键、电源键、音量调节等
b. 与外部硬件兼容:耳机、蓝牙
c. 与操作系统软件兼容:系统时间调节、WLAN设置、LBS定位
d. 与其他app兼容
安装卸载
交叉事件测试
push消息推送
性能测试
常见指标
solopi工具
cpu排查
cpu温度
正常温度:30–40
内存
流畅度
流量测试
电量
冷热启动
用户体验
一、软件测试知识概述
1. 移动端测试是什么?
- 移动测试是指对移动应用进行测试,即实体的特性满足需求的程度
2.移动测试分类
- app功能测试
- 业务逻辑正确性测试
产品文档
- 兼容性测试
系统版本
分辨率
网络情况 - 异常测试
热启动应用
网络切换&中断恢复
电话&信息中断恢复 - 升级&安装&卸载测试
- 健壮性测试
手机资源消耗
流量消耗
崩溃恢复等测试
- 业务逻辑正确性测试
- app自动化测试
通过场景数据的预设,把以人为驱动的测试行为转化为机器执行的一种过程
ps:并不是所有的功能都能进行自动化
- app安全测试
通过安全测试技术,保证app尽可能的不存在安全漏洞
二、 环境搭建
三、ADB命令
1. ADB
adb全名:android debug bridge,是一个标准的C/S结构的工具
包含如下几个部分:
1. Client端,运行在开发机器中,用来发送adb命令
2. Daemon守护进程,运行在调试设备中,即手机&模拟器
3. Server端,作为一个后台进程运行在开发机器中,用来管理pc中Client端和手机的Daemon之间的通信
2.简单命令
-
启动 adb 服务
adb start-server
-
关闭 adb 服务
adb kill-server
-
获取设备号
adb devices
-
获取系统版本
adb -s 设备号 shell getprop ro.build,version.release(多个设备)
adb shell getprop ro.build.version.release(单个设备) -
发送文件到手机
adb push PC端路径/需要发送的文件 手机端存储的路径
C:\Users\liliai>adb push C:\Users\Eric\Desktop\push.txt /sdcard/push.txt
-
从手机拉去文件
adb pull 手机端路径/拉去文件名 PC存储文件路径
C:\Users\liliai>adb pull /sdcard/push.txt C:\Users\Eric\Desktop\push.txt
-
查看手机运行日志
adb logcat
代码实现:将通讯录的日志输出到桌面上
# 此处使用find 对通讯录包名(com.android.contacts) 过滤 C:\Users\liliai>adb logcat | find "com.android.contacts" >>C:\Users\liliai\Desktop\log.txt
-
手机 shell 命令行
adb shell
代码实现:查看cpu
adb shell top
-
获取app启动包名&启动名
adb shell pm list packages # 查看所有的包名 adb shell pm list packages -3 # 列出第三方包 adb shell pm list packages -3 | findstr "girafit" # 根据app名过滤 adb shell dumpsys package <包名> # 查看应用的详细信息
-
安装 app 到手机
adb install 路径/xx.apk
-
卸载 app
adb uninstall 包名
-
获取启动时间
adb shell am start -W 包名/启动名
解释:
TotalTime:app自身启动时间
WaitTime:系统启动应用时间代码实现:查看网易云音乐的启动时间
C:\Users\Eric>adb shell am start -W com.netease.cloudmusic/.activity.MainActivity Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.netease.cloudmusic/.activity.MainActivity } Status: ok Activity: com.netease.cloudmusic/.activity.MainActivity ThisTime: 3748 TotalTime: 3748 Complete ```
四、appium学习
第一个例子
#导入webdriver
from appium import webdriver
import time
# --------------------------------->server 启动参数
desired_caps = {}
# 系统
desired_caps['platformName'] = 'Android'
# 版本
desired_caps['platformVersion'] = '5.1'
# 设备号
desired_caps['deviceName'] = '192.168.52.104:5555'
# app信息
# 包名
desired_caps['appPackage'] = 'com.android.settings'
# 启动名
desired_caps['appActivity'] = '.Settings'
desired_caps['unicodekeyboard']:True
desired_caps['resetkeyboard']:True
# ------------------------------------>启动参数end
# 声明手机驱动对象
driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desired_caps)
# 关闭app,不关闭驱动
driver.close_app()
# 打开app
driver.start_activity("com.android.dialer", ".DialtactsActivity")
# 关闭驱动
driver.quit()
App基本操作API
1. 前置代码
可以将此封装起来,用时候直接调用
from appium import webdriver
desired_caps = {}
desired_caps['platformName'] = 'Android'
desired_caps['platformVersion'] = '5.1'
desired_caps['deviceName'] = '192.168.52.104:5555'
desired_caps['appPackage'] = 'com.android.settings'
desired_caps['appActivity'] = '.Settings'
driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desired_caps)
2.API基础
- 安装app到手机
driver.install_app(app_path)
app_path:脚本机器中.apk的文件路径 - 手机中移除app
driver.remove_app(app_id)
app_id:app的包名 - 判断app是否已安装
driver.is_app_installed(app_id)
app_id: app包名;
返回结果为True(已安装) / False(未安装) - 发送文件到手机
import base64
data = str(base64.b64encode(data.encode(‘utf-8’)),‘utf-8’)
driver.push_file(path,data)
参数:
path:手机设备上的路径(例如:/sdcard/a.txt)
data:文件内数据,要求base64编码
Python3.x中字符都为unicode编码,而b64encode函数的参数为byte类型,需要 先转码;
生成的数据为byte类型,需要将byte转换回去。 - 从手机中拉去文件
import base64
data = driver.pull_file(path) # 返回数据为base64编码
print(str(base64.b64decode(data),‘utf-8’)) # base64解码
参数:
path: 手机设备上的路径
ps:此处在 str 与 betys 的转换,总是出现错误
- 获取当前屏幕内元素结构
driver.page_source
3.定位API
方式 | 语法 | 描述 |
---|---|---|
id | find_element_by_id(id_value) | 通过id定位一个元素 |
find_elements_by_id(id_value) | 通过id定位一组元素 | |
class | find_element_by_class_name(class_value) | 通过class定位一个元素 |
find_elements_by_class_name(class_value) | 通过class定位一组元素 | |
xpath | find_element_by_xpath(xpath_value) | 通过xpath定位一个元素 |
find_elements_by_xpath(xpath_value) | 通过xpath定位一组元素 |
android端常用xpath的定位:
- id: //*[contains(@resource-id,‘id值’)]
- class: //*[contains(@class,‘class值’)]
- text: //*[contains(@text,‘text的部分值’)]
4.显示等待操作
在一个超时时间范围内,每隔一段时间去搜索一次元素是否存在,如果存在返回定位对象,如果不存在知道时间到达,报超时异常
导入:
from selenium.webdriver.support.wait import WebDriverWait
方法:WebDriverWait(driver, timeout, poll_frequency).until(method)
参数:
1. driver:手机驱动对象
2. timeout:搜索超时时间
3. poll_frequency:每次搜索间隔时间,默认时间为0.5s
4. method:定位方法(匿名函数)
5. 匿名函数: ambda x: x
等价于python函数:
def test(x)
return x
实例:
WebDriverWait(driver, timeout, poll_frequency).until(lambda x: x.find_elements_by_id(id_value))
解释:
1. x传入值为:driver,所以才可以使用定位方法.
函数运行过程:
1. 实例化WebDriverWait类,传入driver对象,之后driver对象被赋值给WebDriverWait的一个类变量:self._driver
2. until为WebDriverWait类的方法,until传入method方法(即匿名函数),之后method方法会被传入self._driver
3. 搜索到元素后until返回定位对象,没有搜索到函数until返回超时异常错误.
代码实现:
from selenium.webdriver.support.wait import WebDriverWait # 导入WebDriverWait类
# 超时时间为30s,每隔1秒搜索一次元素是否存在,如果元素存在返回定位对象并退出
search_button = WebDriverWait(driver, 30, 1).until(lambda x: x.find_elements_by_id(com.android.settings:id/search))
search_button.click()
driver.quit()
5. app元素信息操作API
- 点击元素
.click()
- 发送数据到输入框
send_key(val)
代码实现:
# 点击搜索按钮
driver.find_element_by_id("com.android.settings:id/search").click()
# 定位到输入框并输入abc
driver.find_element_by_id("android:id/search_src_text").send_keys("abc")
# 将 *abc*改成中文,输入框不会输入任何值,也不会报错
解决输入中文问题:
server 启动参数增加两个参数设置:
desired_caps[‘unicodeKeyboard’] = True
desired_caps[‘resetKeyboard’] = True
-
清空输入框内容
.clear()
-
获取元素的文本内容
.text
-
获取元素的属性值
get_attribute(属性)
value = ‘name’------> 返回content-desc/text属性值
value = ‘text’------->返回text的属性值
value = ‘className’----->返回 calss 属性值,只有API=>才支持
value = ‘resourceId’------>返回 resource-id属性值,只有API=>18才支持 -
获取元素在屏幕上的坐标
.location
代码实现
# 定位到搜索按钮 get_value = driver.find_element_by_id("com.android.settings:id/search") # 打印搜索按钮在屏幕上的坐标 print(get_value.location) 执行结果: {'y': 44, 'x': 408}
-
获取app包名和启动名
获取包名:current_package
获取启动名:current_activity
代码实现:print(driver.current_package) print(driver.current_activity) 执行结果: com.tencent.news .activity.SplashActivity
6. APP元素事件操作API
-
swip滑动事件
swipe(start_x, start_y, end_x, end_y, duration=None)
ps:从一个坐标滑动到另一个坐标
参数:
1. start_x:起点X轴坐标
2. start_y:起点Y轴坐标
3. end_x: 终点X轴坐标
4. end_y,: 终点Y轴坐标
5. duration: 滑动这个操作一共持续的时间长度,单位:ms# 1.swipe(开始坐标,结束坐标) # 注意,两个坐标的x轴要一样 driver.swipe(198,872,198,321,5000)
-
scroll滑动事件
scroll(origin_el, destination_el)
ps:从一个元素滑动到另一个元素
参数:
1. origin_el:滑动开始的元素
2. destination_el:滑动结束的元素代码实现
end_ele = driver.find_element_by_xpath("//*[contains(@text,('WLAN'))]") start_ele = driver.find_element_by_xpath("//*[contains(@text,('电池'))]") driver.scroll(start_ele, end_ele)
-
drag拖拽事件
drag_and_drop(origin_el, destination_el)
ps:将一个元素拖动到另一个元素上,并替代原本的位置
参数:
1.origin_el:滑动开始的元素
2.destination_el:滑动结束的元素end_ele = driver.find_element_by_xpath("//*[contains(@text,('WLAN'))]") start_ele = driver.find_element_by_xpath("//*[contains(@text,('电池'))]") driver.drag_and_drop(start_ele,end_ele)
-
应用置于后台事件
APP放置后台,模拟热启动
background_app(seconds)
参数:
seconds:停留在后台的时间,单位:秒
7. APP模拟手势高级操作
-
tap():手指轻敲操作
方法:tap(element=None, x=None, y=None).perform()
参数:
1. element:被定位到的元素
2. x:相对于元素左上角的坐标,通常会使用元素的X轴坐标
3. y:通常会使用元素的Y轴坐标
4.perform():发送命令到服务器执行操作代码实现
# 通过元素定位方式敲击屏幕 el = driver.find_element_by_xpath("//*[contains(@text,'WLAN')]") TouchAction(driver).tap(el).perform() # 通过坐标方式敲击屏幕,WLAN坐标:x=155,y=250 TouchAction(driver).tap(x=155,y=250).perform()
-
press()手指按操作
方法:press(el=None, x=None, y=None)
方法:release() # 结束动作,手指离开屏幕
参数:
1.element:被定位到的元素
2.x:通常会使用元素的X轴坐标
3.y:通常会使用元素的Y轴坐标代码实现
# 通过元素定位方式按下屏幕 el = driver.find_element_by_xpath("//*[contains(@text,'WLAN')]") TouchAction(driver).press(el).release().perform() # 通过坐标方式按下屏幕,WLAN坐标:x=155,y=250 TouchAction(driver).tap(x=155,y=250).release().perform()
-
等到操作
wait(ms)
代码实现:
# 点击WLAN driver.find_element_by_xpath("//*[contains(@text,'WLAN')]").click() # 定位到WiredSSID el =driver.find_element_by_id("android:id/title") # 通过元素定位方式长按元素 TouchAction(driver).press(el).wait(5000).perform()
-
手指长按操作
方法:long_press(el=None, x=None, y=None, duration=1000)
参数:
1. element:被定位到的元素
2. x:通常会使用元素的X轴坐标
3. y:通常会使用元素的Y轴坐标
4. duration:持续时间,默认为1000m代码实现:
进入设置,点击WLAN选项,长按WiredSSID选项5秒# 点击WLAN driver.find_element_by_xpath("//*[contains(@text,'WLAN')]").click() # 定位到WiredSSID el =driver.find_element_by_id("android:id/title") # 通过元素定位方式长按元素 TouchAction(driver).long_press(el,duration=5000).release().perform() # 通过坐标方式长按元素,WLAN坐标:x=161,y=242 # TouchAction(driver).long_press(x=161,y=242).perform() # ⚠️ 通过这个方法定位时报服务端错误,怀疑是appium1.7.1版本bug
-
手指移动操作
方法:move_to(el=None, x=None, y=None)
**ps**:move_to在使用坐标进行多次移动时不再像以前一样:使用与前一个坐标的相对坐标,直接输入自己的就行了!!!!
参数:
1. el:定位的元素
2. x:x坐标
3. y:y坐标代码实现:
进入设置,向上滑动屏幕# 定位到存储 el = driver.find_element_by_xpath("//*[contains(@text,'存储')]") # 定位到更多 el1 = driver.find_element_by_xpath("//*[contains(@text,'更多')]") # 元素方式滑动 TouchAction(driver).press(el).move_to(el1).release().perform() # 坐标的方式滑动 # TouchAction(driver).press(x=240,y=600).wait(100).move_to(x=100,y=100).release().perform()
练习:
进入设置,向上滑动屏幕到可见“安全”选项,进入到安全,点击屏幕锁定方式,点击图案,绘制图案
代码实现:from appium import webdriver from appium.webdriver.common.touch_action import TouchAction from selenium.webdriver.support.wait import WebDriverWait import time desired_caps = {} desired_caps['platformName'] = 'Android' desired_caps['platformVersion'] = '5.1' desired_caps['deviceName'] = '192.168.236.102:5555' desired_caps['appPackage'] = 'com.android.settings' desired_caps['appActivity'] = '.Settings' desired_caps['unicodeKeyboard'] = True desired_caps['resetKeyboard'] = True driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', desired_caps) time.sleep(1) def wait_element(xpa): return WebDriverWait(driver, 5, 0.5).until(lambda x: x.find_element_by_xpath(xpa)) try: ele_dc = wait_element("//*[contains(@text,('电池'))]") time.sleep(1) ele_wl = wait_element("//*[contains(@text,('WLAN'))]") driver.scroll(ele_dc, ele_wl, 2000) time.sleep(1) wait_element("//*[contains(@text,('安全'))]").click() time.sleep(1) wait_element("//*[contains(@text,('屏幕锁定方式'))]").click() wait_element("//*[contains(@text,('图案'))]").click() time.sleep(2) # (127,360)(127,617)(384,617) TouchAction(driver).press(x=127,y=360).move_to(x=127,y=617).move_to(x=384,y=617).perform() time.sleep(2) driver.find_element_by_id("com.android.settings:id/footerLeftButton").click() except Exception as e: print(e) finally: driver.quit()
-
获取手机时间
.device_time
print(driver.device_time)
-
获取手机的宽高
通过手机的宽高,可以做一些坐标的操作
get_window_size()代码实现:
print(driver.get_window_size()) # 执行结果: {'height': 800, 'width': 480}
-
发送键到设备
方法:keyevent(keycode, metastate=None):
参数:
keycode:发送给设备的关键代码
metastate:关于被发送的关键代码的元信息,一般为默认值代码实现:
打开设置,按多次音量增加键for i in range(3): driver.keyevent(24)
-
手机操作通知栏
open_notifications()
代码实现:
启动设置,打开通知栏driver.open_notifications()
-
获取手机当前的网络
network_connection
代码实现:
print(driver.network_connection) 执行结果: 6
网络对照表:
-
设置手机网络
方法:set_network_connection(connectionType)
参数:
connectionType:需要被设置成为的网络类型代码实现:
启动设置,设置手机网络为飞行模式driver.set_network_connection(1)
-
手机截图
截取手机当前屏幕,保存指定格式图片到设定位置
方法:get_screenshot_as_file(filename)
参数:
filename:指定路径下,指定格式的图片.代码实现:
打开设置,截图当前页面保存到当前目录import os driver.get_screenshot_as_file(os.getcwd() + os.sep + './screen.png') 执行结果: 当前目录下会生成screen.png文件