本工程是在开源 uiautomator2 和 facebook-wda 项目的基础之上,进行了统一的封装,消除两个项目所提供的接口的差异性,同时也将分散的接口功能进行了聚合,降低接入门槛,简化使用操作,同时也最大化保留了原项目的诸多优点,并可以进行更加深度的定制化拓展。
工程采用业内主流的pytest+allure作为用例运行的基本脚手架和测试报告生成框架,脚本用例采用经典PO模式+业务流程封装+数据分离的思想进行组织和编写。
本工程编写的初衷是为了尽可能的降低UI自动化脚本的编写和使用门槛,着重在元素定位、异常和断言处理上进行了更进一步的策略优化和封装,使得测试人员可以聚焦在元素基本操作的编写和业务流程的逻辑组织上,而无需刻意关注随机的弹窗遮挡或点击按钮无法跳转下一页面的异常toast报错处理等操作,减少流程中过多的自定义断言处理,提高脚本稳定性和复用性(多平台复用、流程复用),降低维护成本,加快脚本编写速度。
git clone https://gitee.com/ran_yong/auto_uiautomator2.git
pip3 install -r requirements.txt
pip3 install ./install/whl/mtn_perf-0.1.0-py3-none-any.whl ./install/whl/mtn_speed-0.1.0-py3-none-any.whl ./install/whl/facebook_wda-1.3.2.dev31-py3-none-any.whl
brew uninstall --ignore-dependencies libimobiledevice
brew uninstall --ignore-dependencies usbmuxd
brew install --HEAD usbmuxd
brew unlink usbmuxd
brew link usbmuxd
brew install --HEAD libimobiledevice
brew install ideviceinstaller
brew link --overwrite ideviceinstaller
brew install git
brew install allure
brew install carthage
brew install android-platform-tools
git clone http://alm.adc.com/ittools/CompTest/_git/UIAutoTest
pip3 install -r requirements.txt
pip3 install ./install/whl/mtn_perf-0.1.0-py3-none-any.whl ./install/whl/mtn_speed-0.1.0-py3-none-any.whl ./install/whl/facebook_wda-1.3.2.dev31-py3-none-any.whl
git clone https://github.com/appium/WebDriverAgent Appium-WebDriverAgent
cd Appium-WebDriverAgent && ./Scripts/bootstrap.sh
open WebDriverAgent.xcodeproj
Fixtures辅助层(driver、data、业务相关的前后置操作)
|
用例层
|
业务流程逻辑层
|
PageObject层(继承BasePage)
|
元素层(元素数据、元素集切换)
WalletUiTest/
- common/ 存放公共文件
- base_page.py: 提供基本页面元素操作
- config_parser.py: 提供配置文件解析方法
- image.py: 提供图像识别方法
- logger.py: 提供日志全局配置方法
- yaml_parser.py: 提供yaml文件解析方法
- utils.py: 提供工具类方法
- install/: 存放修改后的源码或依赖库
- demo/: 文件夹名称表示对应的应用别称(功能域),demo只是示例名称
- vXXX/: 以vXXX表示应用版本号以区分版本用例,每个版本一个文件夹(包含文件夹内的文件结构)
- case/: 存放用例
- data/: 存放测试数据(yaml文件)
- element/: 存放定位元素(yaml文件)
- element_router.py: 元素选择路由,需按照实际补充相关逻辑
- flow/: 存放业务流程逻辑
- image/: 存放需要图像识别的元素图像文件
- page/: 存放PageObject文件
- conftest.py: 放置与应用强关联的Fixtures操作
- debug.py: 做步骤调试
- run.py: 用例统一执行和生成报告
- config.ini: 应用相关配置
- report/: 存放与报告相关的文件
- log: 存放本工程的log日志
- logcat: 存放抓取的系统和应用日志
- screenrecord: 存放用例执行过程的录屏
- screenshot: 存放截图
- raw_data: 存放allure原始数据
- html_report_xxxx: allure生成的html报告,以时间分割保存
- config.ini: 全局相关配置
- conftest.py: 放置通用的Fixtures和Hooks方法
- pytest.ini: pytest运行配置
- README.md: 使用说明文档
由于用例数据常常需要多层级的数据结构,这里选择yaml文件作为本工程的数据文件,示例格式如下:
#用例中数据,以以下方式书写,并存放到data/data.yaml中
test_case_0001:
param1: aaa
param2: bbb
#page中的对应的element定位元素,以以下方式书写,并存放到element/中,文件名以 xxx_zz_element.yaml命名,xxx区分page,zz区分语言区域
element_name1: {text: "这是text文案", resourceId: "com.yyy.wallet:id/xxxx"}
element_name2: {resourceId: "com.yyy.wallet:id/zzzz"}
数据第一层以用例名标识某条用例所使用的数据,这里约定要和用例中的方法名称完全一致,方便后面使用Fixture方法向用例分配数据;
元素数据第一层以元素的英文名称标识,后面元素定位的键值对写在一行中,并用大括号{}括起
想进一步了解yaml语法,参考这篇文章yaml语言教程
可以点击pytest权威教程 进行深入的学习
编写pytest测试样例非常简单,只需要按照下面的规则:
在工程根目录下的conftest.py中,提供了driver(scope=session)和start_stop_app(scope=function)两个fixtures方法。driver需要在用例中手动导入,start_stop_app视场景导入用例,表示在用例执行前会启动被测应用,测试结束后关闭应用
在应用每个版本的目录下的conftest.py中,提供了data(scope=function)fixtures方法,用于测试用例的数据驱动,除此以外,与应用相关的自定义的fixtures方法应全部放在该conftest.py文件中,比如一些特定的前置后置操作等等
#driver和data传入参数
def test_unionpay_0001(self, driver, start_stop_app, data):
union_pay_flow = UnionPayFlow(driver)
union_pay_flow.bind_debit_card_nfc(**data["test_unionpay_0001"])
@pytest.mark.parametrize(('kewords'), [(u"小明"), (u"小红"), (u"小白")])
def test_kewords(self,kewords):
print(kewords)
# 多个参数
@pytest.mark.parametrize("test_input,expected", [
("3+5", 8),
("2+4", 6),
("6*9", 42),
])
def test_eval(test_input, expected):
assert eval(test_input) == expected
class TestExample(object):
@pytest.mark.dependency()
def test_a(self):
assert False
@pytest.mark.dependency()
def test_b(self):
assert False
@pytest.mark.dependency(depends=["TestExample::test_a"])
def test_c(self):
# TestExample::test_a 没通过则不执行该条用例
# 可以跨 Class 筛选
print("Hello I am in test_c")
@pytest.mark.dependency(depends=["TestExample::test_a","TestExample::test_b"])
def test_d(self):
print("Hello I am in test_d")
建议使用release代表生产环境用例,test代表测试环境用例,smoke代表冒烟级别用例
@pytest.mark.release
def test_0001():
pass
#仅执行标记release的用例
pytest -v -m release
#不执行标记release的用例
pytest -v -m "not release"
@pytest.mark.run(order=1)
class TestExample:
def test_a(self):
pass
#使用第三方插件
pip install pytest-rerunfailures #使用插件
pytest --reruns 2 # 失败case重试两次
#run.py args传入下值
def run():
args = ["--reruns", "2", "--reruns-delay", "5"]
pytest.main(args)
Rule 1. 【强制】类、异常名使用驼峰方式命名,缩写不做强制,但推荐驼峰
Rule 2. 【强制】常量使用大写字母,并用下划线分隔单词
Rule 3. 【强制】其余情况一致使用小写字母,并用下划线分隔单词
Rule 4. 【强制】protected成员使用单下划线前缀,private成员使用双下划线前缀
Rule 5. 【强制】禁止使用双下划线开头,双下划线结尾的名字(类似__init__)
正例:
ClassName, ExceptionName
GLOBAL_CONSTANT_NAME, CLASS_CONSTANT_NAME
module_name, package_name, method_name, function_name, global_var_name, instance_var_name, function_parameter_name, local_var_name
_InternalClassName, _INTERNAL_CONSTANT_NAME, _internal_function_name, _protected_member_name, __private_member_name
Rule 1. 【强制】使用4个空格缩进,禁止使用tab缩进
Rule 1. 【强制】文件级定义(类或全局函数)之间隔两个空行,类方法之间隔一个空行
Rule 1. 【强制】圆括号、花括号、方括号内侧都不加空格
Rule 2. 【强制】不要在逗号, 分号, 冒号前面加空格, 但应该在它们后面加(除了在行尾)
Rule 3. 【强制】参数列表, 索引或切片的左括号前不应加空格
Rule 4. 【强制】所有二元运算符前后各加一个空格
Rule 5. 【强制】关键字参数或参数默认值里的等号前后不加空格
Rule 6. 【强制】不要用空格来垂直对齐多行间的标记, 因为这会成为维护的负担(适用于:, #, =等)
正例:
spam(ham[1], {eggs: 2}, [])
if x == 4:
print x, y
x, y = y, x
spam(1)
dict[‘key’] = list[index]
x == 1
def complex(real, imag=0.0): return magic(r=real, i=imag)
foo = 1000 # comment
long_name = 2 # comment that should not be aligned
dictionary = {
“foo”: 1,
“long_name”: 2,
}
反例:
spam( ham[ 1 ], { eggs: 2 }, [ ] )
if x == 4 :
print x , y
x , y = y , x
spam (1)
dict [‘key’] = list [index]
x<1
def complex(real, imag = 0.0): return magic(r = real, i = imag)
foo = 1000 # comment
long_name = 2 # comment that should not be aligned
dictionary = {
“foo” : 1,
“long_name”: 2,
}
使用os.path.join组装文件路径,本工程使用相对路径,相对于run.py所在的路径
os.path.join('.', 'data', 'data.yaml')
读取根目录和应用目录下的config.ini的字段
# 获取平台(Android iOS)
ReadConfig().get_platform
# 获取隐式等待时间
ReadConfig().get_implicitly_wait
# 获取检查错误toast开关状态
ReadConfig().get_check_error_toast
# 获取包名
ReadConfig().get_package_name
# 获取弹窗元素列表
ReadConfig().get_popup_elements
# 获取环境
ReadConfig().get_env
# 获取区域
ReadConfig().get_region
# 获取项目根路径
ReadConfig().get_root_dir
# 查找元素基本方法,聚合了ios、android元素基本定位方法和图像识别方法,若没有识别到会重试retry_times次数
find_element(retry_times=_MAX_RETRY_TIMES, **locator)
# 查找到元素后进行点击操作,默认会在点击操作结束后检查是否弹出异常toast,若不需要检查输入check_toast=False,若需要忽略正常toast输入ignore_toast,
# config.ini中的check_error_toast进行全局控制
find_element_and_click(check_toast=True, ignore_toast=None, **locator)
# 查找到元素后进行长按操作,默认长按1.0s
find_element_and_long_click(duration: float = 1.0, **locator)
# 查找到元素后进行输入操作,plaintext传入输入的文本,通常输入框无需额外进行点击,识别到即可输入文本
find_element_and_input(plaintext=None, **locator)
# 查找到元素后滑动元素,支持"left", "right", "up", "down",steps只用于android,从元素中间开始,1步表示滑动5ms,200步表示滑动1s;
# scale用于android的xpath定位时表示从一边滑至另一边的比例(0, 1.0),scale用于ios时表示滑动元素的宽/高乘以scale的长度
find_element_and_swipe(direction, steps: int = 10, scale: float = 0.8, **locator)
# 查找到元素后拖动元素,与滑动元素不同,这里只支持android,并且需要指定目标点的坐标
find_element_and_drag(*coordinate, **locator)
# 断言元素是否存在,不存在即报错
assert_element_exist(**locator)
# 检查元素存在性,返回bool值
check_element_existence(**locator)
# 断言文字是否存在,不存在即报错
assert_text_exist(plaintext)
# 检查文字存在性,返回bool值
check_text_existence(plaintext)
# 查找到指定的元素后等待元素消失,若超过默认5min时间还没有消失,会断言失败,可依据实际情况修改超时时间,一般用于进度等待
wait_until_element_gone(timeout=300, **locator)
# 滑动屏幕,支持"left", "right", "up", "bottom",android可以设置滑动的比例,滑动距离是屏幕的宽(高)*scale;ios滑动距离固定,
# 上下滑动是从屏幕中间滑到上下边界,左右滑动是从屏幕最左(右)滑到最右(左)
swipe_screen(direction, scale: float = 0.7)
# 向上滚动屏幕直到指定的元素出现
scroll_until_element_appear(retry_times=_MAX_RETRY_TIMES, **locator)
# 滚动屏幕直到达到应用边界(顶部或底部)
scroll_to_boundary(boundary: str = "end", speed: str = "fast")
# 按键操作,Android支持home, back, left, right, up, down, center, menu, search, enter,delete(or del), recent(recent apps),
# volume_up, volume_down,volume_mute, camera, power iOS支持home, volumeUp, volumeDown
press_key(key)
# 打开通知栏
open_notification()
# 打开快速设置栏
open_quick_settings()
# 获取toast
get_toast()
# 屏幕截图
take_screenshot(name="截图")
# 等待,秒
sleep(seconds)
若base_page提供的方法依旧无法满足需求,优先找毕夏提需求,临时可以直接使用u2/wda提供的原始api,引入Fixtures中的driver后可直接调用,相关api使用文档如下:
uiautomator2 使用说明文档
facebook-wda 使用说明文档
注意此规则只适用于android定位
为了写起脚本来更快,u2自定义了一些简化的xpath规则
规则1
//
开头代表原生xpath
规则2
@
开头代表resourceId定位
@smartisanos:id/right_container
相当于//*[@resource-id="smartisanos:id/right_container"]
规则3
^
开头代表正则表达式
^.*道了
相当于 //*[re:match(text(), '^.*道了')]
规则4
灵感来自SQL like
知道%
匹配知道
开始的文本, 相当于 //*[starts-with(text(), '知道')]
%知道
匹配知道
结束的文本,相当于 //*[ends-with(text(), '知道')]
%知道%
匹配包含知道
的文本,相当于 //*[contains(text(), '知道')]
规则5
默认会匹配text 和 description字段
如 搜索
相当于 XPath //*[@text="搜索" or @content-desc="搜索" or @resource-id="搜索"]
特殊说明
$
字符,这个字符在XML中是不合法的,所以全部替换成了-
基本流程: element(image)-->page-->flow-->case-->data
python -m weditor
注意:不同平台提供的定位关键字不同
Android:
- text, textContains, textMatches, textStartsWith
- className, classNameMatches
- description, descriptionContains, descriptionMatches, descriptionStartsWith
- checkable, checked, clickable, longClickable
- scrollable, enabled,focusable, focused, selected
- packageName, packageNameMatches
- resourceId, resourceIdMatches
- index, instance
以上参数都可以单独或组合使用,以下的只能单独使用
- child
- xpath
- image
iOS:
- className
- name, nameContains, nameMatches
- label, labelContains
- value, valueContains
- visible, enabled
- index (index 必须与label,value等结合使用)
以上参数都可以单独(index除外)或组合使用,以下的只能单独使用
- id
- child
- xpath
- predicate
- classChain
- image
# element_router.py
# page和element文件的映射,page的类名作为键,文件名作为值
mapping = {
"CommonPage": "common_cn_element.yaml",
"HomePage": "home_cn_element.yaml",
...
}
# xxx_zz_element.yaml
# resourceId支持简写,消除包名差异,比如com.finshell.wallet:id/tv_order_record,可以简写成tv_order_record
abc: {resourceId: "com.finshell.wallet:id/tv_order_record"}
# 或者(推荐)
abc: {resourceId: "tv_order_record"}
# 组合写法
bcd: {text: "fff", resourceId: "dddeeefff"}
# child写法,传入child关键字,值为字典组成的列表,字典的写法与上面的一致,支持单个和组合,列表长度没有限制
efg: {child: [{className: "android.widget.LinearLayout"},{text: "京津冀互联互通卡"}]}
# right,left,up,down写法,传入right/left/up/down关键字,值为字典组成的列表,字典的写法与上面的一致,支持单个和组合,列表长度限制为2个元素
hij: {right: [{textContains: "身份证", resourceId: "tvName"}, {resourceId: "cetInf"}]}
...
注意:
建议多使用像textContains, textMatches等模糊和正则的写法;resourceId推荐简写的形式,难以定位的元素(只有xpath或者元素重叠)建议用child定位
# xxx_page.py
class XXXPage(BasePage):
def __init__(self, driver):
super(XXXPage, self).__init__(driver)
self.element = ElementRouter.select(self.__class__.__name__)
# ----------------- XXX模块/子页面 -------------------
@allure.step("点击abc")
def click_abc(self):
self.find_element_and_click(**self.element["abc"])
# ----------------- YYY模块/子页面 -------------------
@allure.step("输入bcd")
def input_bcd1(self, bcd1):
self.find_element_and_input(bcd1, **self.element["bcd1"])
@allure.step("输入bcd")
def input_bcd2(self, bcd2):
self.find_element_and_input(bcd2, **self.element["bcd2"])
...
调试某一个步骤
# debug.py
def step(d):
page = XXXPage(d)
page.click_abc()
# xxx_flow.py
class XXXFlow(object):
def __init__(self, driver):
self.xxx_page = XXXPage(driver)
@allure.story('描述是干什么的流程,比如登录账户')
def flow_logic_description(self, **kwargs):
self.xxx_page.click_abc()
self.xxx_page.input_bcd(kwargs["bcd"])
...
# test_xxx.py
@allure.epic("用例一级模块")
class TestXXX:
@allure.feature("用例二级模块")
@allure.title("用例标题")
@pytest.mark.release
def test_xxx_0001(self, driver, start_stop_app, data): # 注意这里传入的fixtures参数,driver必传,data需要的时候再传
xxx_flow = XXXFlow(driver)
xxx_flow.flow_logic_description(**data["test_xxx_0001"])
若需要传递测试数据,data添加数据,传入case层
# data.yaml
test_xxx_0001:
bcd: "abcdefg"
...
注意:
用例执行时默认会自动启动app,执行结束后关闭app(相关的方法可在根目录下的conftest.py中查看),若用例需要额外的前后置操作(比如还原执行前的状态), 请在应用目录下的conftest.py中增加fixtures方法并在对应用例参数中导入
导入logging模块即可(通常用例中不需要再额外打印日志)
import logging
logging.debug()
logging.info()
logging.warning()
logging.error()
logging.critical()
logging.exception()
运行run.py完成测试后会自动处理报告并使用默认浏览器打开报告,若关闭后想再次打开,请在pycharm中找到对应报告的目录下的index.html,右击--Open in Browser
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。