iOS App自动化测试:Python+pytest+appium+allure
一、环境准备:
1.1 Appium环境搭建
- Windows端:Windows端appium环境搭建
- Mac端:Mac端appium环境搭建
总结:appium-doctor 是用来检测环境配的是否完整,安装完成之后,重新打开新的命令行窗口,输入命令 appium-doctor。根据提示安装缺少的内容。
1.2 自动化测试依赖工具:Mac端安装WebDriverAgent
参考文章:【iOS】WebDriverAgent 配置-Build-Test 全过程 (开发者账号可以自己购买,或者可以找公司iOS开发团队借用)
1.3 iOS App元素识别工具:Appium Inspector
- 从官网下载:appium inspector官网 ,且官网有使用说明
- appium inspector使用方法
1、启动Appium Server,设置Host地址为127.0.0.1,Port默认为4723(可改);2、要启动WebDriverAgent,并停止运行针对同一应用的Python测试脚本;3、连接真机/模拟器,并获取相应手机参数及测试APP参数。(获取iOS真机的udid、应用的bund id,可以使用工具py-ios-device 4、打开Appium inspector app,输入参数,然后点击Start Session即可。note:Appium Server中填的就是我们之前启动服务器的参数Desired Capabilities中填写手机参数(及App参数)
ios app使用appium inspactor定位元素的手机参数
{
"platformName": "ios",
"appium:platformVersion": "16.0",
"appium:bundleId": "com.zego.avatar",
"appium:automationName": "XCUITest",
"appium:xcodeOrgId": "xxxx", # 填自己的开发者账号
"appium:xcodeSigningId": "iPhone Developer",
"appium:udid": "401730095f754dd313bbd91c0e000345ab8f638a",
"appium:deviceName": "iPhone X"
}
android app使用appium inspector定位元素
{
"appium:appPackage": "com.zego.goavatar",
"appium:appActivity": "com.zego.goavatar.view.AvatarMainActivity",
"platformName": "Android",
"appium:deviceName": "YSE0221A12000677"
}
二、测试用例
1. pytest 测试框架组织用例
可参考:Python 自动化测试框架unittest与pytest的区别
1.1 pytest用例编写规则:
- 测试文件名必须以"test_“开头或者以”_test"结尾(如test_avatarAPI_iOS.py)
- 测试方法必须以”test_"开头
- 测试类名必须以"Test_"开头
- pytest继承unittest框架,可以兼容unittest用例
1.2 前置后置条件
1.2.1 setup和teardown:pytest提供了模块级、函数级、类级、方法级的setup/teardown
详细可参考:pytest测试用例setup和teardown
- 模块级(setup_mode/teardown_mode)开始于模块始末,作用全局
- 函数级(setup_function/teardown_function)只对函数用例生效(不在类中)
- 类级(setup_class/teardown_class)只在类中的所有用例前/所有用例后运行(在类中)
- 方法级(setup_method/teardown_method)只在类中的每条用例前/每条用例后运行(在类中)
- 类里面的(setup/teardown)运行在调用方法的前后
1.2.2 pytest常用装饰器
参数化装饰器:@pytest.mark.parametrize()
详细可参考pytest-参数化@pytest.mark.parametrize() 可单参数,多参数(笛卡尔积)
# 定义
@pytest.mark.parametrize(self,argnames, argvalues, indirect=False, ids=None, scope=None)):
# 单参数示例
gender = [male,female]
@allure.story("test_creatAvatar_male:创建默认的男性/女性形象")
@pytest.mark.flaky(reruns=0, reruns_delay=3) # 用例执行失败后,再重试2次,间隔3s
@pytest.mark.parametrize('gender', gender) # 性别:gender=[male,female]
def test_createAvatar(self, gender):
装饰器:@pytest.mark.flaky(),用例执行失败后,自动重试
# reruns:重试次数,reruns_delay:重试之间的最小间隔时间
@pytest.mark.flaky(reruns=2, reruns_delay=3) # 用例执行失败后,再重试2次,间隔3s
2.测试用例
2.1 初始化参数说明
# 导入webdriver
from appium import webdriver
# 初始化参数
desired_caps = {
'platformName': 'Android', # 被测手机平台:Android/iOS
'platformVersion': '10', # 手机安卓版本
'deviceName': 'xxx', # 设备名,安卓手机可以随意填写
'appPackage': 'tv.danmaku.bili', # 启动APP Package名称
'appActivity': '.ui.splash.SplashActivity', # 启动Activity名称
'unicodeKeyboard': True, # 使用自带输入法,输入中文时填True
'resetKeyboard': True, # 执行完程序恢复原来输入法
'noReset': True, # 不要重置App,如果为False的话,执行完脚本后,app的数据会清空,比如你原本登录了,执行完脚本后就退出登录了
'newCommandTimeout': 6000, # 超时时间可以设置久一些,太短,用例比较多会报错
'automationName': 'UiAutomator2' # 自动化测试引擎 ,默认Appium
}
# 连接Appium Server,初始化自动化环境
driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
# 退出程序
driver.quit()
2.2 初始化参数示例
# 启动参数
iOS_caps = {
'automationName': 'XCUITest',
'bundleId': 'im.xxx.xxx',
'platformName': 'iOS',
'platformVersion': '16.0',
'xcodeOrgId': 'xxxx',
'xcodeSigningId': 'iPhone Developer',
'newCommandTimeout': "1200000",
'udid': '401730095f754dd313bbd91c0e000345ab8f638a',
'deviceName': 'iPhoneX'
}
2.3 appium操作元素
参考文章:appium操作元素总结Appium之iOS端定位元素中提到: 按查找元素的顺序速度,从快到慢的顺序如下: ios_predicate >> accessibility_id >> class_name >>xpath 且ios_predicate查找元素最为可靠。
appium定位元素的方式:find_element_by_*已被废弃,使用find_element来代替 (见WebDriver定位元素(官网)、Appium Python Client(github)
from selenium.webdriver.common.by import By
text = self.device.find_element(AppiumBy.XPATH, value=self.output_xpath).text
class AppiumBy(By): # 继承的By类
IOS_PREDICATE = '-ios predicate string'
IOS_UIAUTOMATION = '-ios uiautomation'
IOS_CLASS_CHAIN = '-ios class chain'
ANDROID_UIAUTOMATOR = '-android uiautomator'
ANDROID_VIEWTAG = '-android viewtag'
ANDROID_DATA_MATCHER = '-android datamatcher'
ANDROID_VIEW_MATCHER = '-android viewmatcher'
# Deprecated
WINDOWS_UI_AUTOMATION = '-windows uiautomation'
ACCESSIBILITY_ID = 'accessibility id'
IMAGE = '-image'
CUSTOM = '-custom'
定位方式:
class By:
"""
Set of supported locator strategies.
"""
ID = "id"
XPATH = "xpath"
LINK_TEXT = "link text"
PARTIAL_LINK_TEXT = "partial link text"
NAME = "name"
TAG_NAME = "tag name"
CLASS_NAME = "class name"
CSS_SELECTOR = "css selector"
2.4 操作元素
在UI自动化测试中用到的click()、drag_and_drop(origin_el=eleA, destination_el=eleB)方法会多一些。 更多方法如下:
# 元素,以IOS_PREDICATE为定位方式举例
element = self.device.find_element(AppiumBy.IOS_PREDICATE,value=self.RE.element('action_predicate')
# 1.点击元素
element.click()
# 2.清理元素,一般是清空输入框内容
element.clear()
# 3.向元素发送文本,一般是向输入框内输入内容
element.send_keys('hello')
# 4.提交
element.submit()
# 5.拖拽元素从A位置到B的位置
eleA = self.device.find_element(AppiumBy.XPATH,value=self.RE.element('shoes_xpath')
eleB = self.device.find_element(AppiumBy.XPATH,value=self.RE.element('cloth_xpath')
self.device.drag_and_drop(origin_el=eleA, destination_el=eleB)
# 6.通过坐标模拟手点击元素
self.device.tap([(100, 20), (100, 60), (100, 100)], 500)
# tap可参考:[appium点击坐标tap方法及封装]()
# 7.截图1:get_screenshot_as_file(filename)参数filename为截图文件保存的绝对路径,图片为png格式,区域全屏幕,如:
self.device.get_screenshot_as_file('../picture/screenshot.png')
# 8.截图2:save_screenshot(filename)该方法与get_screenshot_as_file()不同的是,参数为文件名称,保存当前屏幕截图到当前脚本所在的文件,区域全屏幕,如:
self.device.save_screenshot('test_02.png')
2.5 获取元素属性
# 1.获取元素的text
element.text
# 2. 获取元素坐标,返回具有元素大小和位置的字典
element.rect
# 3.获取元素坐标(返回字典,x,y轴)
element.location
# 4.获取元素的size(返回字典)
element.size
# 5.获取元素的标签名称
element.tag_name
# 6.返回元素是否被启用
element.is_enabled()
# 7.返回元素是否可选择
element.is_selected()
# 8.元素是否显示
element.is_displayed()
# 9.获取当前会话的活动元素
element = self.device.switch_to.active_element
# 10.获取元素属性
element.get_attribute(value)
# 说明:
# value='name' 返回content-desc或者text属性值,二者只能其一
# value='text'返回text的属性值
# value='className'返回class属性值,要API=>18(Android4.2.1以上即可)
# value='resourceId'返回resource-id的属性值,要API=>18(Android4.2.1以上即可)
2.6 断言
在UI自动化测试中,可断言某按钮是否存在,或者截图再通过图片相似度断言图片是否达到预期(1)断言某个按钮是否存在:
# 如元素element
element = self.device.find_element(AppiumBy.IOS_PREDICATE,value=self.RE.element('action_predicate')
assert element
(2)断言图片相似度 先运行一遍代码,获取截图作为底片,测试时截图作为被测试图片,对比底片和被测图片,获取相似度n(值的范围0~1),再断言n。
截图 appium的截图方法get_screenshot_as_file(filename)截取的是全屏幕,如果要精确截图指定区域,可已使用PIL.Image的crop(box)方法,box位坐标值(x1,y1,x2,y2)
from PIL import Image
def screenshot(self, partName, pictureName):
# 共用方法:根据坐标区域截图
box = (0, 0, 0, 0)
if partName == 'default':
box = (0, 210, 1110, 1430) # 区域1
elif partName == 'hair':
box = (110, 270, 950, 1145) # 区域2
self.device.get_screenshot_as_file(
'../picture/screenshot.png') # 路径和名称,图片是.png格式
image = Image.open('../picture/screenshot.png')
new_image = image.crop(box)
new_image.save(pictureName) # 截取精确区域后的图片
图片相似度 可参考经过测试,“三直方图算法”比较适合我测试的产品
#! /usr/bin/env python3
# _*_ coding:utf-8 _*_
"""
@Auth : Dora
@Date : 2022/6/13 14:14
"""
import cv2
import numpy as np
# 三直方图算法
# 通过得到RGB每个通道的直方图来计算相似度
def classify_hist_with_split(image1, image2, size=(256, 256)):
# 将图像resize后,分离为RGB三个通道,再计算每个通道的相似值
image1 = cv2.resize(image1, size)
image2 = cv2.resize(image2, size)
sub_image1 = cv2.split(image1)
sub_image2 = cv2.split(image2)
sub_data = 0
for im1, im2 in zip(sub_image1, sub_image2):
sub_data += calculate(im1, im2)
sub_data = sub_data / 3
return sub_data
# 计算单通道的直方图的相似值
def calculate(image1, image2):
hist1 = cv2.calcHist([image1], [0], None, [256], [0.0, 255.0])
hist2 = cv2.calcHist([image2], [0], None, [256], [0.0, 255.0])
# 计算直方图的重合度
degree = 0
for i in range(len(hist1)):
if hist1[i] != hist2[i]:
degree = degree + (1 - abs(hist1[i] - hist2[i]) / max(hist1[i], hist2[i]))
else:
degree = degree + 1
degree = degree / len(hist1)
return degree
def runHistHash(image1, image2):
img1 = cv2.imread(image1)
img2 = cv2.imread(image2)
n = classify_hist_with_split(img1, img2)
print('三直方图算法相似度:', n)
return n
if __name__ == "__main__":
image1 = '../picture/p11.png'
image2 = '../picture/p22.png'
runHistHash(image1, image2)
断言插件pytest-assume优化断言,上一个断言失败,可继续执行后续的断言
#!/usr/bin/env python3
#!coding:utf-8
import pytest
@pytest.mark.parametrize(('x', 'y'), [(1, 1), (1, 0), (0, 1)])
def test_simple_assume(x, y):
assert x == y #如果这个断言失败,则后续都不会执行
assert True
assert False
@pytest.mark.parametrize(('x', 'y'), [(1, 1), (1, 0), (0, 1)])
def test_pytest_assume(x, y):
pytest.assume(x == y) #即使这个断言失败,后续仍旧执行
pytest.assume(True)
pytest.assume(False)
3.测试报告
可参考:Python自动化-allure生成测试报告、allure自动化测试报告使用详解
3.1 allure在组织用例
格式如下,括号中的说明内容会展示在测试报告中
@allure.feature("Avartar API测试") # 模块名称
class TestAvatarAPIiOS:
@allure.story('创建默认的男性/女性形象') # 用例名称,对用例的描述
@allure.title('test_createAvatar') # 用例标题
def test_createAvatar(self):
with allure.step('步骤1:打开应用'): # 测试用例操作步骤
print('应用已打开')
with allure.step('步骤2:创建Avatar形象'):
print('已生成Avatar形象')
print('创建默认形象:创建成功')
3.2 执行用例、生成测试报告并截图
import os
import random
from selenium import webdriver
from selenium.webdriver.common.by import By
from time import sleep
import requests
import base64
import hashlib
from WorkWeixinRobot.work_weixin_robot import WWXRobot
# 企业微信群机器发消息
webhook = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=d8243e49-47e3-4420-857e-7ffb01c866ec' # No八哥 群 机器人url
wx_driver = WWXRobot(key=webhook)
def runCase_captureReport(runcmd, port):
# 执行测试用例-生成报告-并给测试报告截图
# 命令执行测试用例
# runcmd : 执行测试用例的命令
# port:指定的测试报告端口
runcmd1 = 'pytest E:/pythonProject/GoAvatarUIAutoTest/testcase/test_GoAvatarUI.py::TestGoAvatar::test_gender_change -s -q --alluredir=../report/result --clean-alluredir' # 指定单条用例
# port = random.randint(1000, 9999) # 生成随机端口
reportcmd = 'allure serve ../report/result -h 192.168.55.183 -p {}'.format(port) # 测试结果生成报告
runcmd2 = 'pytest -v -k change E:/pythonProject/GoAvatarUIAutoTest/testcase/test_GoAvatarUI.py -s -q --alluredir=../report/result --clean-alluredir' # 指定单条用例
runcmdall = 'E:/pythonProject/GoAvatarUIAutoTest/pytest testcase/test_GoAvatarUI.py -s -q --alluredir=./report --clean-alluredir'
rungo = os.popen(runcmd)
runmsg = rungo.read()
print(runmsg)
sleep(3)
reportrun = os.popen(reportcmd)
# 生成allure测试报告并截图
driver = webdriver.Chrome()
report_url = "http://192.168.55.183:{}/index.html".format(port) # 192.168.55.183 为电脑ip地址,{}为端口号
print("测试报告链接:{}".format(report_url))
driver.get(report_url)
driver.maximize_window() # 将浏览器最大化显示
sleep(0.5)
driver.get_screenshot_as_file('../report/reportOverview-{}.png'.format(port)) # 测试报告首页截图
sleep(1)
driver.find_element(By.XPATH, '//*[@id="content"]/div/div[1]/div/ul/li[6]/a/div').click() # 测试报告详情页
sleep(0.5)
driver.find_element(By.XPATH,
'//*[@id="content"]/div/div[2]/div/div[1]/div/div[3]/div/div/div/div[1]/div[1]').click() # 展开测试报告详情
sleep(1)
driver.get_screenshot_as_file('../report/reportPackages-{}.png'.format(port)) # 测试报告详情截图
QA:关于搭建appium环境遇到的问题及解决办法
Windows端:
Q1.全局安装时提示权限问题 chown -R 当前用户名 /usr/local/lib/node_modules
Q2.以下5步解决 opencv4nodejs cannot be found 先装c-make,再安装opencv4nodejs 安装cmake并配置环境变量https://cmake.org/ 安装git并配置环境变量 安装 opencv4nodejs失败的话,可先将镜像设置为淘宝,再执行安装 opencv4nodejs命令 1.设置镜像源 npm config set registry http://registry.npm.taobao.org 2.运行命令 npm i -g npm@6 npm i opencv4nodejs --save npm i -g npm@latest 3.增大git的缓存 git config --global http.postBuffer 524288000 4.运行命令 export OPENCV4NODEJS_DISABLE_AUTOBUILD=1 5.再运行命令 npm i -g opencv4nodejs
Q3.ffmpeg 安装 下载ffmpeg 包,https://ffmpeg.zeranoe.com 解压后,将D:\ffmpeg\bin加入环境变量(我的是D盘,根据自己情况写“\ffmpeg\bin”的完整路径)
Q4.解决 bundletool.jar cannot be found 1.下载bundletool.jar:https://github.com/google/bundletool/releases (授予bundletool.jar访问权限) 2.放在D:\AndroidSDK\bundle-tools下,并重命名为bundletool.jar 将bundletool.jar路径加入环境变量path 3.将.JAR加入PATHEXT
Q5.解决 mjpeg-consumer cannot be found 安装:npm i -g mjpeg-consumer
Q6.解决gst-launch-1.0.exe and/or gst-inspect-1.0.exe cannot be found 1.手动下载安装,下载地址:https://gstreamer.freedesktop.org/download/ 注意:runtime installer 和 development installer 两个应用程序都要下载并安装。 2.安装完成后找到gstreamer路径(安装过程中不能选择安装路径,默认会安装到安装包所在的盘),配置Path系统环境变量:如:E:\gstreamer\1.0\mingw_x86_64\bin(安装路径的bin目录)
Mac端常见报错
Q1.安装opencv4nodejs 1.找个地方新建一个文件夹 nodeOpencv ,打开终端cd进入这个文件夹。 2.⚠️重要⚠️,输入如下命令后回车。 export OPENCV4NODEJS_DISABLE_AUTOBUILD=1 3.接着输入如下后等待5、6、7、8分钟左右即可完成 。 cnpm install --save opencv4nodejs
Q2.解决 idb and idb_companion are not installed brew install facebook/fb/idb-companion 有些库下载失败,就逐个库执行:brew install xx库