做过自动化测试的人应该都会有这样一种体会,要写个自动化 demo 测试用例很容易,但是要真正将自动化测试落地,对成百上千的自动化测试用例实现较好的可复用性和可维护性就很难了。
基于这一痛点,我开发了AppiumBooster
框架。顾名思义,AppiumBooster
基于Appium
实现,但更简单和易于使用;测试人员不用接触任何代码,就可以直接采用简洁优雅的方式来编写和维护自动化测试用例。
原型开发完毕后,我将其应用在当前所在团队的项目上,并在使用的过程中,按照自己心目中理想的自动化测试框架的模样对其进行迭代优化,最终打磨成了一个自己还算用得顺手的自动化测试框架。
本文便是对AppiumBooster
的核心特性及其设计思想进行介绍。在内容组织上,本文的各个部分相对独立,大家可直接选择自己感兴趣的部分进行阅读。
UI 交互是自动化测试的基础,主要分为三部分内容:定位控件、操作控件、检测结果。
定位控件时,统一采用元素 ID 进行定位。这里的 ID 包括accessibility_id
或accessibility_label
,需要在 iOS 工程项目中预先进行设置。
另外,考虑到控件可能出现延迟加载的情况,定位控件时统一执行wait
操作;定位成功后会立即返回控件对象,定位失败时会进行等待并不断尝试定位,直到超时(30 秒)后抛出异常。
wait { id control_id }
源码路径:AppiumBooster/lib/pages/control.rb
根据实践证明,UI 的控件操作基本主要就是点击、输入和滑动,这三个操作基本上可以覆盖绝大多数场景。
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
什么才算是心目中理想的自动化测试框架?我也没有确切的答案。
为什么要登山?
因为山在那里。
项目源码:https://github.com/debugtalk/AppiumBooster