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 Bytext = self.device.find_element(AppiumBy.XPATH, value=self.output_xpath).textclass 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'# DeprecatedWINDOWS_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方法及封装](https://www.cnblogs.com/syw20170419/p/8192629.html)# 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 Imagedef screenshot(self, partName, pictureName):# 共用方法:根据坐标区域截图box = (0, 0, 0, 0)if partName == 'default':box = (0, 210, 1110, 1430) # 区域1elif partName == 'hair':box = (110, 270, 950, 1145) # 区域2self.device.get_screenshot_as_file('../picture/screenshot.png') # 路径和名称,图片是.png格式image = Image.open('../picture/screenshot.png')new_image = image.crop(box)new_image.save(pictureName) # 截取精确区域后的图片
图片相似度
可参考:python OpenCV 图片相似度 5种算法、
Python 五种图片相似度比较方法_佐倉的博客-CSDN博客_python 图片相似度
经过测试,“三直方图算法”比较适合我测试的产品
#! /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 = 0for im1, im2 in zip(sub_image1, sub_image2):sub_data += calculate(im1, im2)sub_data = sub_data / 3return 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 = 0for 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 + 1degree = degree / len(hist1)return degreedef runHistHash(image1, image2):img1 = cv2.imread(image1)img2 = cv2.imread(image2)n = classify_hist_with_split(img1, img2)print('三直方图算法相似度:', n)return nif __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 Trueassert 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八哥 群 机器人urlwx_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库