分享 敏捷实践 (4) - 我们是如何改进 AC

edwardzhou · August 07, 2017 · 1969 hits

敏捷实践 (1) 中,出于介绍目的,测试用例实现的都相对简单。

而实际上验收标准测试用例也并不复杂,百分之八九十的动作无非是:

  • 跳转某个界面
  • 在某个输入框输入一些内容
  • 点击某个按钮
  • 检测某些文本
  • 判断是否处于某个界面
  • 判断是否有弹出框,提示文本

而这些动作 (step),cucumber 是可以用正则表达式进行标准化处理,在然后在各个测试用例中重用。

改进邮箱登录故事 AC 需要用到的 Step


$cat features/US004_login_by_email.feature

Feature: US_004 邮箱登录
  为了正常使用需要登录身份的功能
  作为一个已经用邮箱注册过的用户
  我想要用邮箱和密码登录系统

  @reset_driver
  Scenario: AC_US004_02 登录错误: 正确邮箱+错误密码登录
    Given 我已经用邮箱 test_user@mytest.com 与密码 test123 注册过账号
    When 我在 "主页面" 点击 "登录/注册" 进入 "登录页面"
    And 我在 "邮箱或手机" 输入 "[email protected]"
    And 我在 "密码" 输入 "b123456"
    And 我按下按钮 "登录"
    Then 我应当看到浮动提示 "用户密码不匹配"
.....

$ cat features/step_definitions/steps.rb

Given(/^我已经用邮箱 (.*) 与密码 (.*) 注册过账号$/) do |email, password|
  # sleep(1)
  puts "DEBUG: email: #{email}"
  puts "DEBUG: password: #{password}"
end

When(/^我在 "主页面" 点击 "登录\/注册" 进入 "登录页面"$/) do
  # 等待主页面就绪, 主页面ID 为 home_page
  wait { id('home_page') }
  # 点击 主页面中的 '登录/注册' 按钮,按钮ID为 btn_to_login
  id('btn_to_login').click

  # 检查页面跳转到 登录页面, 登录页面ID为 page_login_account
  wait { id('page_login_account') }
end

When(/^我在 "(.*?)" 输入 "(.*?)"$/) do |input_field, input_value|
  input_id = case input_field
               when '邮箱或手机'
                 'input_username'
               when '密码'
                 'input_password'
               else
                 'unknown'
             end
  input_box = id(input_id)           # 定位指定的输入框
  input_box.clear                    # 清除原来的内容
  input_box.type "#{input_value}\n"  # 输入新内容并回车
end

And(/^我按下按钮 "登录"$/) do
  id('btn_login').click
end

Then(/^我应当看到浮动提示 "(.+)"$/) do |msg|
  msg.strip!
  puts "DEBUG: 期待 #{msg}"
  wait { find(msg) }
end

Then(/^我应当到达 "主页面"$/) do
  wait { id('home_page') }
end

And(/^等待 (\d+) 秒后.*/) do |seconds|
  sleep(seconds.to_i)
end

.

When 我在 "主页面" 点击 "登录/注册" 进入 "登录页面"


这个 step 的目的是我们要在某个界面,点击 某个组件 (按钮或链接),跳转到另一个页面。

When(/^我在 "主页面" 点击 "登录\/注册" 进入 "登录页面"$/) do
  # 等待主页面就绪, 主页面ID 为 home_page
  wait { id('home_page') }
  # 点击 主页面中的 '登录/注册' 按钮,按钮ID为 btn_to_login
  id('btn_to_login').click

  # 检查页面跳转到 登录页面, 登录页面ID为 page_login_account
  wait { id('page_login_account') }
end

这里面有三个小动作:

  1. 确定当前是否在指定界面,是通过查找某个该界面中特有的组件 id 是否存在来判断。
  2. 点击 某个元素,是通过查找到指定 id 的组件,向它发送 click 信号。
  3. 判断当前界面是否是指定界面,同 1。

因此每个界面,我们都设置一个能够标识该界面的一个唯一的 id,便于我们识别当前的界面。 其次每个需要测试交互的组件,都分配一个在该页面唯一的 id。 最后,为上面的硬编码 id 改为对照方式。step 改为正则匹配。

# When(/^我在 "主页面" 点击 "登录\/注册" 进入 "登录页面"$/) do
When(/^我在 "([^"]*)" 点击 "(.*?)" 进入 "(.*?)"$/) do |location, button, dest|
  VIEW_MAPPING = {
    '主页面' => 'home_page',
    '密码登录页面' =>  'page_login_account' 
  }

  BUTTON_MAPPING = {
    '登录/注册' => 'btn_to_login'
  }

  location_id = VIEW_MAPPING[location]
  button_id = BUTTON_MAPPING[button]
  dest_id = VIEW_MAPPING[dest]

  wait do
    puts "DEBUG: #{location} => #{location_id}"
    id(location_id)
  end

  wait do
    puts "DEBUG: #{button} => #{button_id}"
    id(button_id)
  end
  id(button_id).click

  wait do
    puts "DEBUG: #{dest} => #{dest_id}"
    id(dest_id)
  end
end

这个 steps 基本已经通用了。 只要 feature 中按照 我在 "AAA" 点击 "BBB" 进入 "CCC" 这个格式写测试步骤,都能匹配处理。

还有不足的的地方,每次新加按钮或页面 id 时,都需要进入该 step 中添加,而且,其他地方无法重用这些映射定义;另一个是 "=>" 这种映射写法,通不过 Rubocop 的语法检测。

继续优化


把 VIEW_MAPPING BUTTON_MAPPING 移到一个新文件,作为全局的常量(其实 Ruby 中并没有真正的常量定义)。并且,反转映射写法以满足 Rubocop。 Cucumber 会自动加载 features/step_definitions 中所有的文件,无需自己手动 require.

$ cat features/step_definitions/keyword_mapping.rb

##
# 1. 按所在页面进行分类排序
# 2. 不同页面存在相同关键字(button或input), id应相同
# 3. 在下面注释中出现 '已被定义' 的前缀, 是为说明相同的关键字,在所处hash中已被定义,不需要重新定义

VIEW_MAPPING = {
    home_page: '主页面',
    page_more_races: '更多赛事页面',
    page_login_account: '密码登录页面',
    page_login_code: '验证码登录页面',
    ....
}.invert

BUTTON_MAPPING = {
    # 主页面
    btn_to_login: '登录/注册',
    btn_races_1: '第一个赛事',
    btn_race_detail: '赛事详情',
    btn_order: '订单',
    btn_setup: '设置',
    btn_account_security: '账号安全',
    btn_change_password: '修改密码',
    btn_more_races: '更多赛事',

    # 密码登录页面
    btn_bar_right: '注册',
    btn_bar_left: '左上',
    ...
}.invert

改进界面输入的步骤,模版化


When(/^我在 "(.*?)" 输入 "(.*?)"$/) do |input_field, input_value|
  input_id = case input_field
               when '邮箱或手机'
                 'input_username'
               when '密码'
                 'input_password'
               else
                 'unknown'
             end
  input_box = id(input_id)           # 定位指定的输入框
  input_box.clear                    # 清除原来的内容
  input_box.type "#{input_value}\n"  # 输入新内容并回车
end

由于一开始我们就使用了正则匹配,仅是在 ID 这块做了条件硬编码,该为映射处理:

When(/^我在 "(.*?)" 输入 "(.*?)"$/) do |input, value|
  input_id = INPUT_MAPPING[input]
  puts "DEBUG: #{input} => #{input_id}"
  input_box = nil
  wait do
    input_box = id(input_id)
  end
  input_box.clear                # 定位指定的输入框
  input_box.type "#{value}\n".   # 输入新内容并回车
  sleep 1                        # 输入完等待1秒,给模拟器留处理时间
end

为 features/step_definitions/keyword_mapping.rb 添加 INPUT_MAPPING

INPUT_MAPPING = {
    # 密码登录页面
    input_username: '邮箱或手机',
    input_password: '密码',

    # 验证码登录页面
    input_phone: '手机号',
    input_code: '验证码',

    # 手机注册页面
    # 已被定义:手机号,验证码

    # 邮箱注册页面
    input_email: '邮箱',

    # 实名认证
    input_real_name: '真实姓名',
    input_id_card: '身份证号',

    # 修改密码
    input_old_pwd: '旧密码',
    input_new_pwd: '新密码',
    # 赛事
    input_keyword: '赛事关键字',
}.invert

继续改进剩余 step

And(/^我按下按钮 "登录"$/) do
  id('btn_login').click
end

Then(/^我应当看到浮动提示 "(.+)"$/) do |msg|
  msg.strip!
  puts "DEBUG: 期待 #{msg}"
  wait { find(msg) }
end

Then(/^我应当到达 "主页面"$/) do
  wait { id('home_page') }
end

==>

And(/^我[按下|点击]+按钮 "(.*?)"$/) do |button|
  button_id = BUTTON_MAPPING[button]
  wait do
    puts "DEBUG: '#{button}' => #{button_id}"
    element = id(button_id)
    puts "DEBUG: got button: #{button_id}, #{element}"
  end
  id(button_id).click
end

Then(/^我应当看到浮动提示 "(.+)"$/) do |msg|
  msg.strip!
  puts "DEBUG: 期待 #{msg}"
  wait { find(msg) }
end

Then(/^我应当到达 "([^"]*)"$/) do |location|
  location_id = VIEW_MAPPING[location]
  wait do
    puts "DEBUG: #{location} => #{location_id}"
    id(location_id)
  end
end

备注:Cucumber 的 Step 定义中,And Given Then When 这四个都是等价的语法糖,Then 定义的步骤,可以直接在其他步骤中使用。

And(/^我应当看到浮动提示 "(.+)"$/) do |msg|
When(/^我应当看到浮动提示 "(.+)"$/) do |msg|
Then(/^我应当看到浮动提示 "(.+)"$/) do |msg|
Given(/^我应当看到浮动提示 "(.+)"$/) do |msg|

这四种都是等价的。

完整示例


当我们把常用的 Step 整理后,基本上已经能满足 95% 以上的测试用例编写,就连产品,设计也能愉快的按着写 AC 了 这个 step 定义,基本可以直接拿去使用,请叫我 红领巾

$ cat features/step_definitions/steps.rb

Given(/^我已经用邮箱 (.*) 与密码 (.*) 注册过账号$/) do |email, password|
  # sleep(1)
  puts "DEBUG: email: #{email}"
  puts "DEBUG: password: #{password}"
end

Given(/^我在 "([^"]*)" 点击 "(.*?)" 进入 "(.*?)"$/) do |location, button, dest|
  location_id = VIEW_MAPPING[location]
  button_id = BUTTON_MAPPING[button]
  dest_id = VIEW_MAPPING[dest]

  wait do
    puts "DEBUG: #{location} => #{location_id}"
    id(location_id)
  end

  wait do
    puts "DEBUG: #{button} => #{button_id}"
    id(button_id)
  end
  id(button_id).click

  wait do
    puts "DEBUG: #{dest} => #{dest_id}"
    id(dest_id)
  end
end

When(/^我点击 "([^"]*)" [进入|回到]+ "(.*?)"$/) do |button, dest|
  button_id = BUTTON_MAPPING[button]
  dest_id = VIEW_MAPPING[dest]
  wait do
    puts "DEBUG: #{button} => #{button_id}"
    element = id(button_id)
    puts "DEBUG: got button: #{button_id}, #{element}"
  end
  id(button_id).click

  wait do
    puts "DEBUG: #{dest} => #{dest_id}"
    id(dest_id)
  end
end

When(/^我[按下|点击]+按钮 "(.*?)"$/) do |button|
  button_id = BUTTON_MAPPING[button]
  wait do
    puts "DEBUG: '#{button}' => #{button_id}"
    element = id(button_id)
    puts "DEBUG: got button: #{button_id}, #{element}"
  end
  id(button_id).click
end

When(/^点击原生button "(.*?)"$/) do |button|
  wait do
    puts "DEBUG: #{button}"
    element = id(button)
    puts "DEBUG: got button: #{element}"
  end
  id(button).click
end

Given(/^我在 "(.*?)" 输入 "(.*?)"$/) do |input, value|
  input_id = INPUT_MAPPING[input]
  puts "DEBUG: #{input} => #{input_id}"
  input_box = nil
  wait do
    input_box = id(input_id)
  end
  input_box.clear
  input_box.type "#{value}\n"
  sleep 1
end

And(/^等待 (\d+) 秒后.*/) do |seconds|
  sleep(seconds.to_i)
end

Then(/^我应当看到浮动提示 "(.+)"$/) do |msg|
  msg.strip!
  puts "DEBUG: 期待 #{msg}"
  wait { find(msg) }
end

Then(/^我应当到达 "([^"]*)"$/) do |location|
  location_id = VIEW_MAPPING[location]
  wait do
    puts "DEBUG: #{location} => #{location_id}"
    id(location_id)
  end
end

Given(/^我在 "([^"]*)"$/) do |location|
  location_id = VIEW_MAPPING[location]
  wait do
    puts "DEBUG: #{location} => #{location_id}"
    id(location_id)
  end
end

Given(/.*\(创建数据\)$/) do |table|
  params = table.hashes.first
  ac = params.delete('ac').downcase
  result = RemoteFactory.create(ac, params)
  puts result.parsed_body
end

Given(/^我已使用 "([^"]*)" 登录$/) do |value|
  result = RemoteFactory.create('ac_us001', email: value)
  puts result.parsed_body
  puts '回到主页'
  id(BUTTON_MAPPING['回到主页']).click if exists { id(BUTTON_MAPPING['回到主页']) }

  to_login = BUTTON_MAPPING['登录/注册']
  wait do
    puts '登录/注册'
    id(to_login).click
  end

  email_input = INPUT_MAPPING['邮箱或手机']
  password_input = INPUT_MAPPING['密码']
  login = BUTTON_MAPPING['登录']
  wait do
    id(email_input)
  end
  id(email_input).clear
  id(email_input).type "#{value}\n"
  sleep 1

  id(password_input).clear
  id(password_input).type 'test123'
  sleep 1

  id(login).click
end

Given(/^退出登录$/) do
  puts '回到主页'
  id(BUTTON_MAPPING['回到主页']).click if exists { id(BUTTON_MAPPING['回到主页']) }

  bar_left = BUTTON_MAPPING['左上']
  wait do
    puts '左上'
    id(bar_left).click
  end

  setup = BUTTON_MAPPING['设置']
  wait do
    puts '设置'
    id(setup).click
  end

  btn_exit = BUTTON_MAPPING['退出登录']
  wait do
    puts '退出登录'
    id(btn_exit).click
  end

  id('确定').click
end

Given(/^清除数据$/) do
  result = RemoteFactory.create('clear')
  puts result.parsed_body
end

Then(/^我应当看到 "(.*?)" 显示 "(.+)"$/) do |location, msg|
  msg.strip!
  puts "DEBUG: 期待 #{msg}"
  location_id = INPUT_MAPPING[location]
  wait {
    puts "DEBUG: #{location} => #{location_id}"
    id(location_id)
  }
  id(location_id).value.eql?(msg)
end

Then(/^"([^"]*)" 按钮置灰,无法点击$/) do |button_text|
  pending
end

Then(/^"([^"]*)" 按钮无法点击$/) do |button_text|
  pending
end

Then(/^看到的 "([^"]*)" 应为 "([^"]*)"$/) do |text_name, value|
  text_id = TEXT_MAPPING[text_name]
  wait {
    puts "DEBUG: #{text_name} => #{text_id}"
    id(text_id)
  }

  target_value = id(text_id).value.strip
  puts "DEBUG: #{target_value} eql? #{value}"
  raise unless target_value.eql?(value)
end

Then(/^我能看到 "([^"]*)" 这些元素$/) do |elements|
  elements.split(',').each do |element|
    element
    id(TEXT_MAPPING[element])
  end
end

And(/^我在Alert中点击 "(.+)"$/) do |button_text|
  wait(1) do
    tag('UIAAlert')
    button(button_text).click
  end
end

And(/^上传图片$/) do
  id('btn_picker_image').click
  wait(10) do
    id('图库').click
  end
  wait(10) do
    id('好').click
  end
  wait(10) do
    id('Camera Roll').click
  end
  wait do
    tag('XCUIElementTypeCell').click
  end
  sleep 1
end

Then(/^隐藏键盘$/) do
  hide_keyboard('Return')
end

And(/^打印调试 (.+)$/) do |debug_name|
  debug_name.strip!
  if debug_name == 'page'
    page
  elsif debug_name == 'source'
    source
  end
end

Then(/^"([^"]*)" 应隐藏/) do |button_text|
  pending
end

Then(/^我能看到 "(.+)"$/) do |msg|
  msg.strip!
  puts "DEBUG: 期待 #{msg}"
  find(msg)
end

And(/^上滑$/) do
  # swipe start_x: 300, start_y: 300, offest_x: 0, offset_y: -200
  swipe direction: 'up'
end

And(/^下拉刷新$/) do
  # swipe start_x: 300, start_y: 300, offest_x: 0, offset_y: 200
  swipe direction: 'down'
  sleep 1
end

Then(/^我应该找不到 "([^"]*)" 这些元素$/) do |elements|
  elements.split(',').each do |element|
    raise "存在#{element}这个元素" if exists { id(TEXT_MAPPING[element]) }
  end
end

Then(/^应不存在 "([^"]*)"$/) do |button|
  raise "存在#{button}" if exists { id(BUTTON_MAPPING[button]) }
end

Then(/^在 "([^"]*)" 可匹配到 "([^"]*)"$/) do |button, element|
  button_id = BUTTON_MAPPING[button]
  label = ''
  wait do
    puts "DEBUG: #{button} => #{button_id}"
    label = id(button_id).label
    puts "DEBUG: got label: #{label}"
  end
  raise "未匹配到#{element}" unless label.match(element)
end

Then(/^pending:.*$/) do
  pending
end

完整用户故事例子

US_001 邮箱注册


Feature: US_001 邮箱注册
  作为一名非注册用户,我需要用邮箱号,使得我可以完成注册

  @reset_driver
  Scenario: AC_US001_01 注册错误: 错误邮箱注册
    Given 我在 "主页面"
    When 我在 "主页面" 点击 "登录/注册" 进入 "密码登录页面"
    And 我点击 "注册" 进入 "手机注册页面"
    And 我点击 "使用邮箱注册" 进入 "邮箱注册页面"
    And 我在 "邮箱" 输入 "aa@desh"
    And 我在 "密码" 输入 "test123"
    And 我按下按钮 "完成"
    Then 我应当看到浮动提示 "您的电子邮件格式不正确"

  Scenario: AC_US001_02 下一步按钮是灰色状态
    Given 我在 "邮箱" 输入 ""
    Then "完成" 按钮置灰,无法点击

  Scenario: AC_US001_03 成功跳转到 手机注册页面
    Given 我在 "邮箱注册页面"
    When 我按下按钮 "使用手机注册"
    Then 我应当到达 "手机注册页面"

  Scenario: AC_US001_04 成功跳转到 密码登录页面
    Given 我点击 "使用邮箱注册" 回到 "邮箱注册页面"
    When 我按下按钮 "我已有账号"
    Then 我应当到达 "密码登录页面"

  Scenario: AC_US001_05 注册错误 邮箱已注册
    Given 我已经用邮箱 test@gmail.com 注册过账号 (创建数据)
      | ac       | clear | email          |
      | AC_US001 | true  | test@gmail.com |
    When 我点击 "注册" 进入 "手机注册页面"
    And 我点击 "使用邮箱注册" 回到 "邮箱注册页面"
    And 我在 "邮箱" 输入 "[email protected]"
    And 我在 "密码" 输入 "test123"
    And 我按下按钮 "完成"
    Then 我应当看到浮动提示 "邮箱已被使用"

#  Scenario: AC_US001_06  备注:重复ac,与AC_US001_01重复

  Scenario: AC_US001_07 注册错误 密码格式错误
    Given 我在 "邮箱" 输入 "[email protected]"
    When 我在 "密码" 输入 "123456"
    And 我按下按钮 "完成"
    Then 我应当看到浮动提示 "密码格式不正确"

  Scenario: AC_US001_08 注册成功
    Given 我在 "邮箱" 输入 "[email protected]"
    When 我在 "密码" 输入 "test123456"
    And 我按下按钮 "完成"
    Then 我应当到达 "主页面"

US-006 忘记密码 - 邮箱找回

Feature: US-006 忘记密码-邮箱找回
  作为一名忘记密码的用户,我需要用已认证的邮箱, 使得我能够找回密码

  @reset_driver
  Scenario: AC-US006-01 没有任何输入
    Given 我在 "主页面" 点击 "登录/注册" 进入 "密码登录页面"
    And 我点击 "忘记密码" 进入 "忘记密码页面"
    And 我按下按钮 "使用邮箱找回密码"
    Then "下一步" 按钮置灰,无法点击

  Scenario: AC-US006-02 没有任何输入 点击获取验证码
    When 我按下按钮 "获取验证码"
    Then 我应当看到浮动提示 "您的电子邮件格式不正确"

  Scenario: AC-US006-03 错误格式的邮箱 点击获取验证码
    And 我在 "邮箱" 输入 "test@aa"
    And 我按下按钮 "获取验证码"
    Then 我应当看到浮动提示 "您的电子邮件格式不正确"

  Scenario: AC-US006-04 错误格式的邮箱 点击获取验证码
    And 我在 "邮箱" 输入 "ricky@aa"
    And 我按下按钮 "获取验证码"
    Then 我应当看到浮动提示 "您的电子邮件格式不正确"

#  Scenario: AC_US006_05  备注:重复ac,与AC_US006_07重复

  Scenario: AC-US006-06 未输入邮箱 但输入了验证码
    When 我在 "邮箱" 输入 ""
    And 我在 "验证码" 输入 "aaa"
    And 我按下按钮 "获取验证码"
    Then 我应当看到浮动提示 "您的电子邮件格式不正确"

  Scenario: AC-US006-07 输入正确的邮箱 及正确的验证码
    Given 我已经用邮箱 test@aa.com 注册过账号 (创建数据)
      |ac         |clear|email|
      |AC_US006_07|true |test@aa.com|
    And 我在 "邮箱" 输入 "[email protected]"
    And 我按下按钮 "获取验证码"
    And 我在 "验证码" 输入 "123456"
    And 隐藏键盘
    And 我按下按钮 "下一步"
    Then 我应当到达 "输入密码页面"

  Scenario: AC-US006-08 输入正确的邮箱 及正确的验证码
    Given 我已经用邮箱 test@aa.com 注册过账号 (创建数据)
      |ac         |clear|email|
      |AC_US006_08|true |test@aa.com|
    And 我在 "密码" 输入 "123456"
    And 我按下按钮 "完成"
    Then 我应当看到浮动提示 "密码格式不正确"

  Scenario: AC-US006-09 输入正确的邮箱 及正确的验证码
    Given 我按下按钮 "回到主页"
    When 我在 "主页面" 点击 "登录/注册" 进入 "密码登录页面"
    And 我点击 "忘记密码" 进入 "忘记密码页面"
    And 我按下按钮 "使用邮箱找回密码"
    And 我在 "邮箱" 输入 "[email protected]"
    And 我按下按钮 "获取验证码"
    And 我在 "验证码" 输入 "123456"
    And 我按下按钮 "下一步"
    And 我在 "密码" 输入 "a123456"
    And 我按下按钮 "完成"
    Then 我应当到达 "密码登录页面"

最后,为了确保测试用例每次都能正确,我们专门部署一个后端持续集成版本,额外提供了一组用于重置/创建测试用例数据,用于支持 APP 测试。

No Reply at the moment.
You need to Sign in before reply, if you don't have an account, please Sign up first.