美文网首页敏捷实践
敏捷实践(4) - 我们是如何改进AC

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

作者: edwardzhq | 来源:发表于2017-04-20 15:52 被阅读110次

    敏捷实践(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 我在 "邮箱或手机" 输入 "test_user@mytest.com"
        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 我在 "邮箱" 输入 "test@gmail.com"
        And 我在 "密码" 输入 "test123"
        And 我按下按钮 "完成"
        Then 我应当看到浮动提示 "邮箱已被使用"
    
    #  Scenario: AC_US001_06  备注:重复ac,与AC_US001_01重复
    
      Scenario: AC_US001_07 注册错误 密码格式错误
        Given 我在 "邮箱" 输入 "test@gmail.com"
        When 我在 "密码" 输入 "123456"
        And 我按下按钮 "完成"
        Then 我应当看到浮动提示 "密码格式不正确"
    
      Scenario: AC_US001_08 注册成功
        Given 我在 "邮箱" 输入 "test1@gmail.com"
        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 我在 "邮箱" 输入 "test@aa.com"
        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 我在 "邮箱" 输入 "test@aa.com"
        And 我按下按钮 "获取验证码"
        And 我在 "验证码" 输入 "123456"
        And 我按下按钮 "下一步"
        And 我在 "密码" 输入 "a123456"
        And 我按下按钮 "完成"
        Then 我应当到达 "密码登录页面"
    

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

    相关文章

      网友评论

        本文标题:敏捷实践(4) - 我们是如何改进AC

        本文链接:https://www.haomeiwen.com/subject/kdakzttx.html