在 敏捷实践(1) 中,出于介绍目的,测试用例实现的都相对简单。
而实际上验收标准测试用例也并不复杂,百分之八九十的动作无非是:
- 跳转某个界面
- 在某个输入框输入一些内容
- 点击某个按钮
- 检测某些文本
- 判断是否处于某个界面
- 判断是否有弹出框,提示文本
而这些动作(step), cucumber是可以用正则表达式进行标准化处理,在然后在各个测试用例中重用。
改进邮箱登录故事AC需要用到的Step
$cat features/US004_login_by_email.feature
Feature: US_004 邮箱登录
为了正常使用需要登录身份的功能
作为一个已经用邮箱注册过的用户
我想要用邮箱和密码登录系统
@reset_driver
Scenario: AC_US004_02 登录错误: 正确邮箱+错误密码登录
Given 我已经用邮箱 [email protected] 与密码 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
这里面有三个小动作:
- 确定当前是否在指定界面,是通过查找某个该界面中特有的组件id是否存在来判断。
- 点击 某个元素, 是通过查找到指定id的组件,向它发送click信号。
- 判断当前界面是否是指定界面, 同 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 我已经用邮箱 [email protected] 注册过账号 (创建数据)
| ac | clear | email |
| AC_US001 | true | [email protected] |
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 我已经用邮箱 [email protected] 注册过账号 (创建数据)
|ac |clear|email|
|AC_US006_07|true |[email protected]|
And 我在 "邮箱" 输入 "[email protected]"
And 我按下按钮 "获取验证码"
And 我在 "验证码" 输入 "123456"
And 隐藏键盘
And 我按下按钮 "下一步"
Then 我应当到达 "输入密码页面"
Scenario: AC-US006-08 输入正确的邮箱 及正确的验证码
Given 我已经用邮箱 [email protected] 注册过账号 (创建数据)
|ac |clear|email|
|AC_US006_08|true |[email protected]|
And 我在 "密码" 输入 "123456"
And 我按下按钮 "完成"
Then 我应当看到浮动提示 "密码格式不正确"
Scenario: AC-US006-09 输入正确的邮箱 及正确的验证码
Given 我按下按钮 "回到主页"
When 我在 "主页面" 点击 "登录/注册" 进入 "密码登录页面"
And 我点击 "忘记密码" 进入 "忘记密码页面"
And 我按下按钮 "使用邮箱找回密码"
And 我在 "邮箱" 输入 "[email protected]"
And 我按下按钮 "获取验证码"
And 我在 "验证码" 输入 "123456"
And 我按下按钮 "下一步"
And 我在 "密码" 输入 "a123456"
And 我按下按钮 "完成"
Then 我应当到达 "密码登录页面"