原文:http://coryschires.com/ten-tips-for-writing-better-cucumber-steps/
紧接着在以 s
结尾的复数后面加一个?
,比如
Then /^the users? should receive an email$/ do
# ...
end
这个 ?
其实就是正则表达式的问号,可以表示重复前面内容的 0 次或一次,也就要么不出现,要么出现一次。所以上面的例子可以匹配 user
和 users
。
在元组前面加 ?:
, 比如: (?:some text)
。 匹配 some text, 但不捕获匹配的文本,也不给此分组分配组号。这表示这个括号里的东西不会被当作参数传递给你的 step definition
。 比如
And /^once the files? (?:have|has) finished processing$/ do
# ...
end
这可以匹配:
once the file have finished processing (不合理)
once the files have finished processing
once the file has finished processing
once the files has finished processing (不合理)
或者
When /^(?:I|they) create a profile$/ do
# ...
end
这可以匹配:
I create a profile
they create a profile
我们常常会用到 是
或者 不是
的判断。 比如
I should see the message
I should not see the message
那其实我们可以把 not 做为一个供选择的元组,合并这两个 step。比如:
Then /^I should( not)? see the following columns: "([^"]*)"$/ do |negate, columns|
within('table thead tr') do
columns.split(', ').each do |column|
negate ? page.should_not(have_content(column)) : page.should(have_content(column))
end
end
end
常常看到这样的 step:
Given I have "potatoes" in my cart
这个括号很形象直观,突出了 potatoes 是我要传递的参数。但是:
Given I have "7" items in my cart
这样就很糟糕了。数字类型的还是建议不需要用括号括起来。
transforms
写出更加聪明干净的正则表达式对于数字的处理,我们一般会这样:
Then /^I have (\d+) items? in my cart$/ do |item_count|
item_count.to_i.times { ... }
end
Cucumber 把任何东西当成字符串传递。所以你必须谨记,在传递数字并进行比较的时候,要用 to_i
。
但是,自己来转就会很容易忘记。所以 Cucumber 提供了 Transform
。
CAPTURE_A_NUMBER = Transform /^\d+$/ do |number|
number.to_i
end
Then /^I have (#{CAPTURE_A_NUMBER}) items? in my cart$/ do |item_count|
item_count.times { ... }
end
这样我们就不用主动的调用 to_i
方法,而且因为 CAPTURE_A_NUMBER
的抽离,我们也更好维护这段匹配。
去冗的一个方法就是把重复的步骤或者重复的定义变成一个方法。比如,得到当前用户:
def current_user
User.find_by_email('current_user@example.com')
end
或者把有想通逻辑的步骤包装起来。比如:
def within_lightbox(opts = {sleep: 0} )
sleep(opts[:sleep])
within_frame("prettyPhotoIframe") { yield }
end
这样我们的 step 定义就可以干净优美:
Then /^some stuff should be visible in the lightbox$/ do
within_lightbox { page.should have_content('Some Stuff') }
end
我们可以把这些方法加入到 features/support/[whatever].rb
里去, cucumber 在运行的时候,会把这些方法读入到它的 world
中去。
当然,有得必有失,你封装了逻辑,也就使得步骤不是那么直观了,想要明白它在做什么,就必须到你定义的方法里去找。所以利弊要自己权衡。
比如:
When /^the request for an expedited decision should be canceled$/ do
manuscript.should_not be_expedited
step %{"#{current_user.email}" should receive an email with subject "Expedited decision request canceled"}
end
但也不要乱用,比如:
When /^I update the email template to read "([^"]*)"$/ do |text|
fill_in("email_template[text], with: text)
click_button("Save changes and close")
# Rather than...
# step %{I fill in "email_template[text]" with "#{text}"}
# step %{I press "Save changes and close"}
end
^
或者 $
来匹配通常我们会这样写:
Given /^I am an admin user$/ do |item_count|
# ...
end
我们使用^
或者 $
来固定我们要匹配的东西。上面的例子就是完全匹配 I am an admin user
, 多一个少一个都不行。大多数情况下,这样很好。但是:
Then /^wait (\d+) seconds/ do |seconds|
sleep(seconds.to_i)
end
有时候,其实不必要固定结尾。这样,上面的语句就可以匹配:
Then wait 2 seconds for the revenue statistics to finish loading
Then wait 5 seconds while the document is converted
一旦,测试数据很大,就请使用 table,否则等待你的就是重复的步骤。
Given "Frankie's Hams" are selling for $25:
And the following orders have been placed:
| buyer email | quantity |
| eddy@example.com | 3 |
| matt@example.com | 2 |
Cucumber 有很多小技巧,但是请记住,你去掉了重复,就必然增加了复杂。比如
Given /^(?:I|they) have opened (?:a|an) ([^\s]+) invitation$/ do |invitation_type|
invitation = Factory("#{invitation_type}_invitation".to_sym, recipient: current_user, to: current_user.email)
reset_mailer
eval "ApplicationMailer.invite_#{invitation_type}(invitation).deliver"
open_email(current_user.email)
end
上面的语句可以匹配:
Given I have opened a reviewer invitation
Given I have opened an editor invitation
但是这个表达式实在过于复杂,理解起来也很困难,不如拆开成两个更好。
这篇文章写的不错,在我在公司项目实践中都运用到了。不过不得不说一句,测试工具再好,也需要开发的配合。如果代码本身就复杂或者重复实现,那你也没办法。