作过自动化测试的人应该都会有这样一种体会,要写个自动化demo测试用例很容易,可是要真正将自动化测试落地,对成百上千的自动化测试用例实现较好的可复用性和可维护性就很难了。android
基于这一痛点,我开发了AppiumBooster
框架。顾名思义,AppiumBooster
基于Appium
实现,但更简单和易于使用;测试人员不用接触任何代码,就能够直接采用简洁优雅的方式来编写和维护自动化测试用例。ios
原型开发完毕后,我将其应用在当前所在团队的项目上,并在使用的过程当中,按照本身心目中理想的自动化测试框架的模样对其进行迭代优化,最终打磨成了一个本身还算用得顺手的自动化测试框架。git
本文即是对AppiumBooster
的核心特性及其设计思想进行介绍。在内容组织上,本文的各个部分相对独立,你们可直接选择本身感兴趣的部分进行阅读。github
UI交互是自动化测试的基础,主要分为三部份内容:定位控件、操做控件、检测结果。express
定位控件时,统一采用元素ID进行定位。这里的ID包括accessibility_id
或accessibility_label
,须要在iOS工程项目中预先进行设置。json
另外,考虑到控件可能出现延迟加载的状况,定位控件时统一执行wait
操做;定位成功后会当即返回控件对象,定位失败时会进行等待并不断尝试定位,直到超时(30秒)后抛出异常。缓存
wait { id control_id }复制代码
源码路径:AppiumBooster/lib/pages/control.rb
ruby
根据实践证实,UI的控件操做基本主要就是点击、输入和滑动,这三个操做基本上能够覆盖绝大多数场景。bash
scrollToDisplay
: 根据指定控件的坐标位置,对屏幕进行上/下/左/右
滑动操做,直至将指定控件展现在屏幕中click
: 经过控件ID定位到指定控件,并对指定控件进行click
操做;若指定控件不在当前屏幕中,则先执行scrollToDisplay
,再执行click
操做type(text)
: 在指定控件中输入字符串;若指定控件不在当前屏幕中,则先执行scrollToDisplay
,再执行输入操做tapByCoordinate
: 先执行scrollToDisplay
,确保指定控件在当前屏幕中;获取指定控件的坐标值,而后对坐标进行tap
操做scroll(direction)
: 对屏幕进行指定方向的滑动源码路径:AppiumBooster/lib/pages/actions.rb
微信
每次执行一步操做后,须要对执行结果进行判断,以此来肯定测试用例的各个步骤是否执行成功。
当前,AppiumBooster
采用控件的ID做为检查对象,并统一封装到check_elements(control_ids)
方法中。
在实际使用过程当中,须要先肯定当前步骤执行完成后的跳转页面的特征控件,即当前步骤执行前不存在该控件,但执行成功后的页面中具备该控件。而后在操做步骤描述的expectation
属性中指定特征控件的ID。
具体地,在指定控件ID的时候还能够配合使用操做符(!
,||
,&&
),以此实现多种复杂场景的检测。典型的预期结果描述形式以下:
A
: 预期控件A存在;!A
: 预期控件A不存在;A||B
: 预期控件A或控件B至少存在一个;A&&B
: 预期控件A和控件B同时存在;A&&!B
: 预期控件A存在,但控件B不存在;!A&&!B
: 预期控件A和控件B都不存在。源码路径:AppiumBooster/lib/pages/inner_screen.rb
对于自动化测试而言,自动化测试用例的组织与管理是最为重要的部分,直接关系到自动化测试用例的可复用性和可维护性。
通过综合考虑,AppiumBooster
从三个层面来描述测试用例,从低到高分别是step
、feature
和testcase
;描述方式推荐使用YAML
格式。
首先是对于单一操做步骤的描述。
从UI层面来看,每个操做步骤均可以概括为三个方面:定位控件、操做控件和检查结果。
AppiumBooster
的作法是,将App根据功能模块进行拆分,每个模块单首创建一个YAML
文件,并保存在steps
目录下。而后,在每一个模块中以控件为单位,分别进行定义。
现以以下示例进行详细说明。
---
AccountSteps:
enter Login page:
control_id: tablecellMyAccountLogin
control_action: click
expectation: btnForgetPassword
input test EmailAddress:
control_id: txtfieldEmailAddress
control_action: type
data: leo.lee@debugtalk.com
expectation: sectxtfieldPassword
check if coupon popup window exists(optional):
control_id: inner_screen
control_action: has_control
data: btnViewMyCoupons
expectation: btnClose
optional: true复制代码
其中,AccountSteps
是steps模块名称,用于区分不一样的steps模块,方便在features
模块中进行引用。
描述单个步骤时,有三项是必不可少的:步骤名称、控件ID(control_id
)和控件操做方式(control_action
)。当控件操做方式为输入(type
)时,则还需指定data
属性,即输入内容。
在检查步骤执行结果方面,可经过在expectation
属性中指定控件ID进行实现,前面在预期结果检查
一节中已经详细介绍了使用方法。该属性能够置空或不进行填写,至关于不对当前步骤进行检测。
另外还有一个optional
属性,对步骤指定该属性并设置为true时,当前步骤的执行结果不影响整个测试用例。
各个模块的单一操做步骤定义完毕后,虽然能够直接将多个步骤进行组合造成对测试场景的描述,即测试用例,可是操做起来会过于局限细节;特别是当测试用例较多时,可维护性是一个很大的问题。
AppiumBooster
的作法是,将App根据功能模块进行拆分,每个模块单首创建一个YAML
文件,并保存在features
目录下。而后,在每一个模块中以功能点为单位,经过对steps模块中定义好的操做步骤进行引用并组合,便可实现对功能点的描述。
以系统登陆
功能为例,功能点的描述可采用以下形式。
---
AccountFeatures:
login with valid test account:
- AccountSteps | enter My Account page
- AccountSteps | enter Login page
- AccountSteps | input test EmailAddress
- AccountSteps | input test Password
- AccountSteps | login
- AccountSteps | close coupon popup window(optional)
login with valid production account:
- AccountSteps | enter My Account page
- AccountSteps | enter Login page
- AccountSteps | input production EmailAddress
- AccountSteps | input production Password
- AccountSteps | login
- AccountSteps | close coupon popup window(optional)
logout:
- AccountSteps | enter My Account page
- SettingsSteps | enter Settings page
- AccountSteps | logout复制代码
其中,AccountFeatures
是features模块名称,用于区分不一样的features模块,方便在testcase
中进行引用。
在引用steps模块的操做步骤时,须要同时指定steps模块名称和操做步骤的名称,并以|
进行分隔。
在功能点描述的基础上,AppiumBooster
就能够在第三个层面,简单清晰地描述测试用例了。
具体作法很简单,针对每一个测试用例建立一个YAML
文件,并保存在testcases
目录下。而后,经过对features模块中定义好的功能点描述进行引用并组合,便可实现对测试用例的描述。
一样的,在引用features模块的功能点时,也须要同时指定features模块名称和功能点的名称,并以|
进行分隔。
以下示例即是实现了在商城中购买商品的整个流程,包括切换国家、登陆、选择商品、添加购物车、下单完成支付等功能点。
---
Buy Phantom 4:
- SettingsFeatures | initialize first startup
- SettingsFeatures | Change Country to China
- AccountFeatures | login with valid account
- AccountFeatures | Change Shipping Address to China
- StoreFeatures | add phantom 4 to cart
- StoreFeatures | finish order
- AccountFeatures | logout复制代码
另外,在某些测试场景中可能须要重复进行某一个功能点的操做。虽然能够将须要重复的步骤多写几回,但会显得比较累赘,特别是重复次数较多时更是麻烦。
AppiumBooster
的作法是,在测试用例的步骤中可指定执行次数,并以|
进行分隔,以下例所示。
---
Send random text messages:
- SettingsFeatures | initialize first startup
- AccountFeatures | login with valid test account
- MessageFeatures | enter follower user message page
- MessageFeatures | send random text message | 100复制代码
基本上,YAML
测试用例引擎已经能够很好地知足组织和管理自动化测试用例的需求。
但考虑到部分用户会偏向于使用表格的形式,由于表格看上去更直观一些,AppiumBooster
同时还支持CSV
格式的测试用例引擎。
采用表格来编写测试用例时,只须要在任意表格工具,包括Microsoft Excel、iWork Numbers、WPS等,按照以下形式对测试用例进行描述。
而后,将表格内容另存为CSV
格式的文件,并放置于testcases
目录中便可。
能够看出,CSV
格式的测试用例和YAML
格式的测试用例是等价的,二者包含的信息内容彻底相同。
在具体实现上,AppiumBooster
在执行测试用例以前,也会将两个测试用例引擎的测试用例描述转换为相同的数据结构,而后再进行统一的操做。
统一转换后的数据结构以下所示:
{
"testcase_name": "Login and Logout",
"features_suite": [
{
"feature_name": "login with valid account",
"feature_steps": [
{"control_id": "btnMenuMyAccount", "control_action": "click", "expectation": "tablecellMyAccountSystemSettings", "step_desc": "enter My Account page"},
{"control_id": "tablecellMyAccountLogin", "control_action": "click", "expectation": "btnForgetPassword", "step_desc": "enter Login page"},
{"control_id": "txtfieldEmailAddress", "control_action": "type", "data": "leo.lee@debugtalk.com", "expectation": "sectxtfieldPassword", "step_desc": "input EmailAddress"},
{"control_id": "sectxtfieldPassword", "control_action": "type", "data": 12345678, "expectation": "btnLogin", "step_desc": "input Password"},
{"control_id": "btnLogin", "control_action": "click", "expectation": "tablecellMyMessage", "step_desc": "login"},
{"control_id": "btnClose", "control_action": "click", "expectation": nil, "optional": true, "step_desc": "close coupon popup window(optional)"}
]
},
{
"feature_name": "logout",
"feature_steps": [
{"control_id": "btnMenuMyAccount", "control_action": "click", "expectation": "tablecellMyAccountSystemSettings", "step_desc": "enter My Account page"},
{"control_id": "tablecellMyAccountSystemSettings", "control_action": "click", "expectation": "txtCountryDistrict", "step_desc": "enter Settings page"},
{"control_id": "btnLogout", "control_action": "click", "expectation": "uiviewMyAccount", "step_desc": "logout"}
]
}
]
}复制代码
yaml2csv
)既然CSV
格式的测试用例和YAML
格式的测试用例是等价的,那么二者之间的转换也就容易实现了。
当前,AppiumBooster
支持将YAML
格式的测试用例转换为CSV
格式的测试用例,只须要执行一条命令便可。
$ ruby start.rb -c "yaml2csv" -f ios/testcases/login_and_logout.yml复制代码
在自动化测试执行过程当中,应尽可能对测试用例执行过程进行记录,方便后续对问题根据定位和追溯。
当前,AppiumBooster
已实现的记录形式有以下三种:
因为Appium
分为Server端和Client端,所以AppiumBooster
在记录日志的时候也将日志分为了三份:
appium_server.log
: Appium Server端的日志,这部分日志是由Appium框架
打印的appium_booster.log
: 包括测试环境初始化和测试用例执行记录,这部分日志是由AppiumBooster
中采用logger模块打印的client_server.log
: 同时记录AppiumBooster
和Appium框架
的日志,至关于appium_server.log
和appium_booster.log
的并集,优势在于能够清晰地看到测试用例执行过程当中Client端和Server端的通信交互过程另外,当测试用例执行失败时,AppiumBooster
会将执行失败的步骤截图和日志提取出来,单独保存到errors
文件夹中,方便问题追溯。
具体地,每次执行测试前,AppiumBooster
会在指定的results
目录下建立一个以当前时间(%Y-%m-%d_%H:%M:%S
)命名的文件夹,存储结构以下所示。
2016-08-28_16:28:48
├── appium_server.log
├── appium_booster.log
├── client_server.log
├── errors
│ ├── 16_31_29_btnLogin.click.dom
│ ├── 16_31_29_btnLogin.click.png
│ ├── 16_32_03_btnMenuMyAccount.click.dom
│ └── 16_32_03_btnMenuMyAccount.click.png
├── screenshots
│ ├── 16_30_34_tablecellMyAccountLogin.click.png
│ ├── 16_30_41_txtfieldEmailAddress.type_leo.lee@debugtalk.com.png
│ ├── 16_30_48_sectxtfieldPassword.type_123456.png
│ ├── 16_31_29_btnLogin.click.png
│ └── 16_32_03_btnMenuMyAccount.click.png
└── xmls
├── 16_30_34_tablecellMyAccountLogin.click.dom
├── 16_30_41_txtfieldEmailAddress.type_leo.lee@debugtalk.com.dom
├── 16_30_48_sectxtfieldPassword.type_123456.dom
├── 16_31_29_btnLogin.click.dom
└── 16_32_03_btnMenuMyAccount.click.dom复制代码
对于每个测试步骤的截图和DOM,存储文件命名格式为%H_%M_%S_ControlID.ControlAction
。采用这种命名方式有两个好处:
在执行自动化测试时,某些状况下可能会形成Appium Server
出现异常状况(e.g. 500 error),并影响到下一次测试的执行。
为了不这类状况,AppiumBooster
在每次执行测试前,会强制性地对Appium Server
进行重启。方式也比较简单暴力,运行测试以前先检查系统是否有bin/appium
的进程在运行,若是有,则先kill掉该进程,而后再启动Appium Server
。
须要说明的是,因为Appium Server
的启动须要必定时间,为了防止运行Appium Client
时Appium Server
还未初始化完毕,所以启动Appium Server
后最好能等待一段时间(e.g. sleep 10s)。
iOS/Android
模拟器在模拟器中运行一段时间后,也会存在缓存数据和文件,可能会对下一次测试形成影响。
为了不这类状况,AppiumBooster
在每次执行测试前,会先删除已存在的模拟器,而后再用指定的模拟器配置建立新的模拟器。
对于iOS模拟器,AppiumBooster
经过调用xcrun simctl
命令的方式来对模拟器进行操做,基本原理以下所示。
# delete iOS simulator: xcrun simctl delete device_id
$ xcrun simctl delete F2F53866-50A5-4E0F-B164-5AC1702AD1BD
# create iOS simulator: xcrun simctl create device_type device_type_id runtime_id
$ xcrun simctl create 'iPhone 5' 'com.apple.CoreSimulator.SimDeviceType.iPhone-5' 'com.apple.CoreSimulator.SimRuntime.iOS-9-3'复制代码
其中,device_id
/device_type_id
/runtime_id
这些属性值能够经过执行xcrun simctl list
命令获取获得。
$ xcrun simctl list
== Device Types ==
iPhone 5s (com.apple.CoreSimulator.SimDeviceType.iPhone-5s)
iPhone 6 (com.apple.CoreSimulator.SimDeviceType.iPhone-6)
== Runtimes ==
iOS 8.4 (8.4 - 12H141) (com.apple.CoreSimulator.SimRuntime.iOS-8-4)
iOS 9.3 (9.3 - 13E230) (com.apple.CoreSimulator.SimRuntime.iOS-9-3)
== Devices ==
-- iOS 8.4 --
iPhone 5s (E1BD9CC5-8E95-408F-849C-B0C6A44D669A) (Shutdown)
-- iOS 9.3 --
iPhone 5s (BAFEFBE1-3ADB-45C4-9C4E-E3791D260524) (Shutdown)
iPhone 6 (F23B3F85-7B65-4999-9F1C-80111783F5A5) (Shutdown)
== Device Pairs ==复制代码
除了以上基础特性,AppiumBooster
还支持一些辅助特性,能够加强测试框架的使用体验。
在某些场景下,测试用例执行时须要动态获取数值。例如,注册帐号的测试用例中,每次执行测试用例时须要保证用户名为未注册的,常见的作法就是在注册用户名中包含时间戳。
AppiumBooster
的作法是,能够在测试步骤的data
字段中,传入Ruby表达式,格式为${ruby_expression}
。在执行测试用例时,会先对ruby_expression
进行eval
计算,而后用计算获得的值做为实际参数。
回到刚才的注册帐号测试用例,填写用户名的步骤就能够按照以下形式指定参数。
input test EmailAddress:
control_id: txtfieldEmailAddress
control_action: type
data: ${Time.now.to_i}@debugtalk.com
expectation: sectxtfieldPassword复制代码
实际执行测试用例时,data
就会参数化为1471318368@debugtalk.com
的形式。
对于某些配置参数,例如系统的登陆帐号密码等,虽然能够直接填写到测试用例的steps
中,可是终究不够灵活。特别是当存在多个测试用例引用同一个参数时,涉及到参数改动时就须要同时修改多个地方。
更好的作法是,将此类参数提取出来,在统一的地方进行配置。在AppiumBooster
中,能够在config.yml
文件中配置全局参数。
---
TestEnvAccount:
UserName: test@debugtalk.com
Password: 123456
ProductionEnvAccount:
UserName: production@debugtalk.com
Password: 12345678复制代码
而后,在测试用例的steps
就能够采用以下形式对全局参数进行引用。
---
AccountSteps:
input test EmailAddress:
control_id: txtfieldEmailAddress
control_action: type
data: ${config.TestEnvAccount.UserName}
expectation: sectxtfieldPassword
input test Password:
control_id: sectxtfieldPassword
control_action: type
data: ${config.TestEnvAccount.Password}
expectation: btnLogin复制代码
在执行测试用例时,有时候可能会存在这样的场景:某个步骤做为非必要步骤,当其执行失败时,咱们并不想将测试用例断定为不经过。
基于该场景,在测试用例设计表格中增长了optional
参数。该参数值默认不用填写。但若是在某个步骤对应的optional栏填写了true值后,那么该步骤就会做为非必要步骤,其执行结果不会影响整个用例的执行结果。
例如,在电商类APP中,某些帐号有优惠券,登陆系统后,会弹出优惠券的提示框;而有的帐号没有优惠券,登陆后就不会有这样的弹框。那么关闭优惠券弹框的步骤就能够将其optional
参数设置为true。
---
AccountSteps:
close coupon popup window(optional):
control_id: btnClose
control_action: click
expectation: !btnViewMyCoupons
optional: true复制代码
AppiumBooster
经过在命令行中进行调用。
$ ruby start.rb -h
Usage: start.rb [options]
-p, --app_path <value> Specify app path
-t, --app_type <value> Specify app type, ios or android
-f, --testcase_file <value> Specify testcase file(s)
-d, --output_folder <value> Specify output folder
-c, --convert_type <value> Specify testcase converter, yaml2csv or csv2yaml
--disable_output_color Disable output color复制代码
指定执行测试用例时支持多种方式,常见的几种使用方式示例以下:
$ cd ${AppiumBooster}
# 执行指定的测试用例文件(绝对路径)
$ ruby run.rb -p "ios/app/test.zip" -f "/Users/Leo/MyProjects/AppiumBooster/ios/testcases/login.yml"
# 执行指定的测试用例文件(相对路径)
$ ruby run.rb -p "ios/app/test.zip" -f "ios/testcases/login.yml"
# 执行全部yaml格式的测试用例文件
$ ruby run.rb -p "ios/app/test.zip" -f "ios/testcases/*.yml"
# 执行ios目录下全部csv格式的测试用例文件
$ ruby run.rb -p "ios/app/test.zip" -t "ios" -f "*.csv"复制代码
将YAML格式的测试用例转换为CSV格式的测试用例:
$ ruby start.rb -c "yaml2csv" -f ios/testcases/login_and_logout.yml复制代码
什么才算是心目中理想的自动化测试框架?我也没有确切的答案。
为何要爬山?
由于山在那里。
原文连接:debugtalk.com/post/build-…
笔名九毫,英文名Leo Lee。
专一于软件测试领域和测试开发技术,享受在墙角安静地debug,也喜欢在博客上分享文字。
我的博客:debugtalk.com
我的微信公众号:DebugTalk