Appium 打造心目中理想的自动化测试框架 (AppiumBooster)

debugtalk · 2016年09月07日 · 最后由 Catfish 回复于 2016年12月17日 · 4884 次阅读

前言

做过自动化测试的人应该都会有这样一种体会,要写个自动化 demo 测试用例很容易,但是要真正将自动化测试落地,对成百上千的自动化测试用例实现较好的可复用性和可维护性就很难了。

基于这一痛点,我开发了AppiumBooster框架。顾名思义,AppiumBooster基于Appium实现,但更简单和易于使用;测试人员不用接触任何代码,就可以直接采用简洁优雅的方式来编写和维护自动化测试用例。

原型开发完毕后,我将其应用在当前所在团队的项目上,并在使用的过程中,按照自己心目中理想的自动化测试框架的模样对其进行迭代优化,最终打磨成了一个自己还算用得顺手的自动化测试框架。

本文便是对AppiumBooster的核心特性及其设计思想进行介绍。在内容组织上,本文的各个部分相对独立,大家可直接选择自己感兴趣的部分进行阅读。

UI 交互基础

UI 交互是自动化测试的基础,主要分为三部分内容:定位控件、操作控件、检测结果。

控件定位

定位控件时,统一采用元素 ID 进行定位。这里的 ID 包括accessibility_idaccessibility_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

测试用例引擎(YAML)

对于自动化测试而言,自动化测试用例的组织与管理是最为重要的部分,直接关系到自动化测试用例的可复用性和可维护性。

经过综合考虑,AppiumBooster从三个层面来描述测试用例,从低到高分别是stepfeaturetestcase;描述方式推荐使用YAML格式。

steps(测试步骤描述)

首先是对于单一操作步骤的描述。

从 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 时,当前步骤的执行结果不影响整个测试用例。

features(功能点描述)

各个模块的单一操作步骤定义完毕后,虽然可以直接将多个步骤进行组合形成对测试场景的描述,即测试用例,但是操作起来会过于局限细节;特别是当测试用例较多时,可维护性是一个很大的问题。

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 模块名称和操作步骤的名称,并以|进行分隔。

testcases(测试用例描述)

在功能点描述的基础上,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

测试用例引擎(CSV)

基本上,YAML测试用例引擎已经可以很好地满足组织和管理自动化测试用例的需求。

但考虑到部分用户会偏向于使用表格的形式,因为表格看上去更直观一些,AppiumBooster同时还支持CSV格式的测试用例引擎。

testcases(测试用例描述)

采用表格来编写测试用例时,只需要在任意表格工具,包括 Microsoft Excel、iWork Numbers、WPS 等,按照如下形式对测试用例进行描述。

AppiumBooster CSV Testcase example

然后,将表格内容另存为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已实现的记录形式有如下三种:

  • logger 模块:可指定日志级别对测试过程进行记录
  • 截图功能:测试用例运行过程中,在每个步骤执行完成后进行截图
  • DOM source:测试用例运行过程中,在每个步骤执行完成后保存当前页面的 DOM 内容

测试结果存储

由于Appium分为 Server 端和 Client 端,因此AppiumBooster在记录日志的时候也将日志分为了三份:

  • appium_server.log: Appium Server 端的日志,这部分日志是由Appium框架打印的
  • appium_booster.log: 包括测试环境初始化和测试用例执行记录,这部分日志是由AppiumBooster中采用 logger 模块打印的
  • client_server.log: 同时记录AppiumBoosterAppium框架的日志,相当于appium_server.logappium_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。采用这种命名方式有两个好处:

  • 文件通过时间排序,对应着测试用例执行的步骤顺序
  • 可以在截图或 DOM 中直观地看到每一步操作指令对应的执行结果

环境初始化

Appium Server

在执行自动化测试时,某些情况下可能会造成Appium Server出现异常情况(e.g. 500 error),并影响到下一次测试的执行。

为了避免这类情况,AppiumBooster在每次执行测试前,会强制性地对Appium Server进行重启。方式也比较简单暴力,运行测试之前先检查系统是否有bin/appium的进程在运行,如果有,则先 kill 掉该进程,然后再启动Appium Server

需要说明的是,由于Appium Server的启动需要一定时间,为了防止运行Appium ClientAppium 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还支持一些辅助特性,可以增强测试框架的使用体验。

Data 参数化

在某些场景下,测试用例执行时需要动态获取数值。例如,注册账号的测试用例中,每次执行测试用例时需要保证用户名为未注册的,常见的做法就是在注册用户名中包含时间戳。

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参数。该参数值默认不用填写。但如果在某个步骤对应的 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

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 19 条回复 时间 点赞

不错,类似这种模式,我认为有两个必须解决的核心问题:1,是稳定性,2,是容错能力

我之前也做了一个简单一点的:

贴下我的处理方式:

--- 
- 
  element_info: "com.dgm.user:id/homepage_my_ll"
  findElemtType: id
  operate_type: click
  test_id: 1001
  test_intr: 个人中心
- 
  element_info: "com.dgm.user:id/my_order_layout"
  findElemtType: id
  operate_type: click
- 
  element_info: "com.dgm.user:id/activity_myallorder_cashpay_rl"
  findElemtType: id
  find_type: find

  • 第一个入口,最后一个是检查点

我有个问题,框架 id 一般不会这么规范都去取名的,所以 id 去校验以及定位是不是有点偏颇

为什么要登山?
因为山在那里。
好有哲理,文章看完了。

我现在也在写框架,也是集大家之所长,再自己写点东西。

分为四大层:用例层,任务层,公共 api 层,对象库层。个人感觉如果做成表格形式是不是太重了,当然给功能测试用,易懂性的确高

看到这句话我能感觉到局限性很大很大,“定位控件时,统一采用元素 ID 进行定位”

#2 楼 @lose 挺相似的,只是我的做法将用例描述分为了三层

#3 楼 @mads
#5 楼 @utopia

当前我是只用 id 去定位控件,这里的 id 也并非一定要通过添加 accesssibility_id 才可以,没有添加的时候,text 也是可以的。
另外,在 AppiumBooster 中添加其它定位方式也是挺容易实现的。

#4 楼 @mads 表格的确会比较重,也不能很好的进行重用,所以我后面做了 YAML 的形式,现在也是主要用 YAML 这种方式编写用例。然后同时支持将 YAML 转换为表格,方便查看。

#10 楼 @debugtalk 代码 ruby 的,表示看不懂

#10 楼 @debugtalk 可以在定位跟校验上扩展一下,当然肯定就是封装一下的事情

@debugtalk 楼主我有两个问题:
1、需要判断结果的情况该如何处理,比如一步操作的结果可能会返回 3 种结果:1、2、3,当 1 的时候需要执行操作 A,2 的时候执行操作 B,3 的时候执行操作 C
2、执行时需要前一步的结果,比如输入验证码的过程,需要先获取文本框中的验证码,然后再在编辑框中输入

不知道楼主框架中对这两种情况是如何处理的,本人才疏学浅还望楼主赐教

#13 楼 @xinufo
对于第一个问题;这种情况最好的做法还是从数据源头解决。通过精确地控制测试数据,来让实际结果满足预期,毕竟,自动化测试就是要实现精确的操作。
对于第二个问题,在 Web 性能测试中用的比较多,通常叫做参数关联。在 app 的自动化中也可以采用类似的思想,读取控件的属性值后,后一步作为参数进行使用。当前 AppiumBooster 中还没有实现这种机制。

#9 楼 @mads 是的 看头像就知道是途牛

@debugtalk 请教一下:

这是目前看到基于 Appium 做 DSL 封装做的最好的一个项目。
从纯过程式操作来看,已经极为精简了并具有表达力了。

正在犹豫是沿着这类过程式操作展开 Appium,还是基于 Page Object Model 展开。

可以不写脚本的,现在这类的工具陆续出来了,比如国内有个 TestWriter。

excel 实现页面对象的抓取录入,可以支持多种元素类型,define_method 动态生成定义对象的方法就行了,测试步骤觉得还是代码里写会比较好,类似 page object,对应多个 case 的参数化测试会比楼主这样的设计好处理感觉,而且代码肯定是会要写的,因为测试里不仅是点击操作,还会有一些特殊方法的处理,当然你可以写到一个工具类去,不断补充,自己写框架执行逻辑的话,稳定性需要保证,BDD 的话 rspec 可用。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册