在本系列的上一篇文章中,我通过系统登录这一典型功能点,演示了编写自动化测试脚本的整个流程,并对测试脚本进行了初步优化。

在本文中,我将重点介绍如何对自动化测试脚本实现⎡工程化⎦的组织和管理。

测试脚本⎡工程化⎦

首先说下什么是测试脚本的工程化。

通过之前的工作,我们已经可以让单个自动化测试用例正常运行起来了。然而,这还只算是一个demo,一切才刚刚开始。

试想,一个项目的自动化测试用例少则数百,多则成千上万。如何将这些自动化测试用例组织起来?如何实现更好的可重用机制?如何实现更好的可拓展机制?这些都还是我们当前的demo所不具备的,也是我们需要通过“工程化”手段进行改造的原因。

引入Minitest/RSpec

在Ruby中,说到测试首先就会想到Minitest或RSpec,这是Ruby中用的最多的两个测试框架。通过这些框架,我们可以很好地实现对Ruby测试用例的管理。

同样地,由于我们的自动化测试脚本是采用Ruby编写的,因此我们也可以使用Minitest/RSpec来管理我们的自动化测试用例。

基于该想法,我们采用RSpec对之前的系统登录测试用例进行工程结构初始化。对于熟悉Ruby编程,或者有一定代码基础的同学而言,很自然地,可以将测试用例框架初始化为如下结构。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
├── Gemfile
├── android
│   └── appium.txt
├── common
│   ├── requires.rb
│   └── spec_helper.rb
└── ios
    ├── appium.txt
    └── spec
        └── login_spec.rb

Gemfile中,指定了项目依赖的库。

1
2
3
4
5
6
# filename: Gemfile
source 'https://gems.ruby-china.org'

gem 'rspec'
gem 'appium_lib'
gem 'appium_console'

common/spec_helper.rb中,定义了模拟器和RSpec初始化相关的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# filename: common/spec_helper.rb

def setup_driver
  return if $driver
  appium_txt = File.join(Dir.pwd, 'ios', 'appium.txt')
  caps = Appium.load_appium_txt file: appium_txt
  Appium::Driver.new caps
end

def promote_methods
  Appium.promote_appium_methods RSpec::Core::ExampleGroup
end

setup_driver
promote_methods

RSpec.configure do |config|

  config.before(:each) do
    $driver.start_driver
    wait { alert_accept }
  end

  config.after(:each) do
    driver_quit
  end

end

common/requires.rb中,实现了对相关库文件的引用。

1
2
3
4
5
6
7
8
# filename: common/requires.rb

# load lib
require 'rspec'
require 'appium_lib'

# setup rspec
require_relative 'spec_helper'

ios/appium.txt中,对iOS模拟器信息和测试包路径进行了配置。

1
2
3
4
5
[caps]
platformName = "ios"
deviceName = "iPhone 6s"
platformVersion = "9.3"
app = "/Users/Leo/MyProjects/AppiumBooster/ios/app/test.app"

ios/spec/目录中,则是测试用例的内容。例如,ios/spec/login_spec.rb对应的就是系统登录的测试用例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# filename: ios/spec/login_spec.rb
require_relative '../../common/requires'

describe 'Login' do

  it 'with valid account' do
    wait { id('btnMenuMyAccount').click }
    wait { id 'uiviewMyAccount' }

    wait { id('tablecellMyAccountLogin').click }
    wait { id 'uiviewLogIn' }

    wait { id('txtfieldEmailAddress').type 'leo.lee@debugtalk.com' }
    wait { id('sectxtfieldPassword').type '123321' }
    wait { id('btnLogin').click }
    wait { id 'tablecellMyMessage' }
  end

end

通过以上代码结构初始化,我们的测试用例框架的雏形就形成了。接下来,在Terminal中切换到项目根目录,然后通过rspec ios命令就可以执行ios目录中的测试用例了。

1
2
3
4
5
➜ rspec ios
.

Finished in 2 minutes 7.2 seconds (files took 1.76 seconds to load)
1 example, 0 failures

完整的代码请参考debugtalk/AppiumBooster1.FirstTest分支

添加第二条测试用例

现在,我们尝试往当前的测试框架中添加第二条测试用例。

例如,第二条测试用例要实现启动后从当前地区切换至中国。那么,就可以新增ios/spec/change_country_spec.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# filename: ios/spec/change_country_spec.rb
require_relative '../../common/requires'

describe 'Change country' do

  it 'from Hong Kong to China' do
    wait { id('btnMenuMyAccount').click }
    wait { id 'uiviewMyAccount' }

    wait { id('tablecellMyAccountSystemSettings').click }
    wait { id 'txtCountryDistrict' }

    wait { id('txtCountryDistrict').click }
    wait { id 'uiviewSelectCountry' }

    wait { id('tablecellSelectCN').click }

    wait { id('btnArrowLeft').click }
    wait { id 'uiviewMyAccount' }
  end

end

完整的代码请参考debugtalk/AppiumBooster2.SecondTest分支

现在我们凝视已经添加的两个测试用例,有发现什么问题么?

是的,重复代码太多。在每一步操作中,都要用id来定位控件,还要用wait来实现等待机制。

除此之外,当前代码最大的问题就是测试用例与控件映射杂糅在一起。造成的后果就是,不管是控件映射发生变动,还是测试用例需要修改,都要来修改这一份代码,维护难度较大。

重构:测试用例与控件映射分离

基于以上问题,我们首要的改造任务就是将测试用例与控件映射进行分离。

考虑到常用的控件操作方法就只有几个(clicktype),因此我们可以将控件操作方法单独封装为一个模块,作为公共模块。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
module Actions

  def click
    wait { @found_cell.click }
  end

  def type(text)
    wait { @found_cell.type text }
  end

end

然后,将APP中每一个页面封装为一个模块(module),将页面中的控件映射为模块的静态方法(method),并通过include机制引入方法模块。

例如,登录页面就可以封装为如下代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
module Pages
  module Login
    class << self

      include Actions

      def field_Email_Address
        @found_cell = wait { id 'txtfieldEmailAddress' }
        self
      end

      def field_Password
        @found_cell = wait { id 'sectxtfieldPassword' }
        self
      end

      def button_Login
        @found_cell = wait { id 'btnLogin' }
        self
      end

    end
  end
end

module Kernel
  def login
    Pages::Login
  end
end

这里还用到了一点Ruby元编程技巧,就是将页面模块封装为一个方法,并加入到Kernel模块下。这样做的好处就是,我们可以在项目的任意地方直接通过login.button_Login.click这样的形式来对控件进行操作了。

完成以上改造后,系统登录测试用例就可以采用如下形式进行编写了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
describe 'Login' do

  it 'with valid account' do
    # switch to My Account page
    my_account.button_My_Account.click
    inner_screen.has_control 'uiviewMyAccount'

    # enter login page
    my_account.button_Login.click
    inner_screen.has_control 'uiviewLogIn'

    # login
    login.field_Email_Address.type 'leo.lee@debugtalk.com'
    login.field_Password.type '123321'
    login.button_Login.click
    inner_screen.has_control 'tablecellMyMessage'
  end

end

完整的代码请参考debugtalk/AppiumBooster3.RefactorV1分支

To be continued …

经过这一轮重构,我们的测试用例与控件映射已经实现了分离,测试用例的可重用性与可扩展性也得到了极大的提升。

然而,在当前模式下,所有的测试用例仍然是以代码形式存在的,新增和修改测试用例时都需要到工程目录下编辑Ruby文件。

那有没有一种可能,我们只需要在表格中维护自动化测试用例(如下图),然后由代码来读取表格内容就可以自动执行测试呢?

AppiumBooster overview testcase examples

是的,这就是我们对测试框架进行⎡工程化⎦改造的下一个形态,也就是AppiumBooster现在的样子。

在下一篇文章中,我们再进行详细探讨。