iOS App自动化测试:Python+pytest+appium+allure

一、环境准备:

1.1 Appium环境搭建

  1. Windows端:Windows端appium环境搭建
  2. Mac端:Mac端appium环境搭建

总结:appium-doctor 是用来检测环境配的是否完整,安装完成之后,重新打开新的命令行窗口,输入命令 appium-doctor。根据提示安装缺少的内容。

1.2 自动化测试依赖工具:Mac端安装WebDriverAgent

参考文章:【iOS】WebDriverAgent 配置-Build-Test 全过程 (开发者账号可以自己购买,或者可以找公司iOS开发团队借用)

1.3 iOS App元素识别工具:Appium Inspector

  1. 从官网下载:appium inspector官网 ,且官网有使用说明
  2. 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自动化测试 unity ios自动化测试预安装应用_自动化

ios自动化测试 unity ios自动化测试预安装应用_自动化_02

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用例编写规则:
  1. 测试文件名必须以"test_“开头或者以”_test"结尾(如test_avatarAPI_iOS.py)
  2. 测试方法必须以”test_"开头
  3. 测试类名必须以"Test_"开头
  4. pytest继承unittest框架,可以兼容unittest用例
1.2 前置后置条件
1.2.1 setup和teardown:pytest提供了模块级、函数级、类级、方法级的setup/teardown

详细可参考:pytest测试用例setup和teardown

  1. 模块级(setup_mode/teardown_mode)开始于模块始末,作用全局
  2. 函数级(setup_function/teardown_function)只对函数用例生效(不在类中)
  3. 类级(setup_class/teardown_class)只在类中的所有用例前/所有用例后运行(在类中)
  4. 方法级(setup_method/teardown_method)只在类中的每条用例前/每条用例后运行(在类中)
  5. 类里面的(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库