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

前言

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

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

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

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

UI交互基础

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

控件定位

定位控件时,统一采用元素ID进行定位。这里的ID包括accessibility_idaccessibility_label,须要在iOS工程项目中预先进行设置。json

另外,考虑到控件可能出现延迟加载的状况,定位控件时统一执行wait操做;定位成功后会当即返回控件对象,定位失败时会进行等待并不断尝试定位,直到超时(30秒)后抛出异常。缓存

wait { id control_id }复制代码

源码路径:AppiumBooster/lib/pages/control.rbruby

控件操做

根据实践证实,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

测试用例引擎(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复制代码

总结

什么才算是心目中理想的自动化测试框架?我也没有确切的答案。

为何要爬山?
由于山在那里。


原文连接:debugtalk.com/post/build-…

项目源码:github.com/debugtalk/A…


关于做者

笔名九毫,英文名Leo Lee。

专一于软件测试领域和测试开发技术,享受在墙角安静地debug,也喜欢在博客上分享文字。

我的博客:debugtalk.com

我的微信公众号:DebugTalk

DebugTalk

相关文章
相关标签/搜索