在第三章中,我们故意没有给你完整的故事,而是仅仅给你展示了关键字集合。因为当你开始利用Cucumber工作时,他们是你需要的基础结构单元,而且我们也希望你能尽快开始使用。现在是时候改善你的能力了。
当你开始编写cucumber feature时,你的主要目标是保证可读性。否则,一个读者很容易感觉到他们在阅读计算机程序而不是一份说明文档,这是我们极力要避免的情况。毕竟,如果你的feature不能让非程序员轻松阅读,你也可能仅仅使用普通、陈旧的ruby代码来编写你的测试。
富有表现力的场景的关键点是有一个领域语言中的健康语法来表达你的需求。也就是说,仅仅使用Gherkin关键字集合经常使你的feature重复、混乱、难懂。在本章的最后,你会了解所有关于Gherkin语法的知识,给你所有需要的工具,能够帮助你编写清晰、易读的Cucumber可接受的测试。我们也会向你展示如何使用tags和文件夹,当你些更多的feature时,能够保持有条理的形式。
首先我们想要专注于帮助你删除重复的混乱。我们想要向你展示如何使用scenario outlines和data tables来使得你的Gherkin场景更加易读,但是我们从另一个叫做Background的关键字开始。
5.1 Background
在feature文件中,一个background节允许你指定step集合,这个集合对文件中的每个场景都是一样的。为了防止在每个场景中一遍遍地重复,你需要移除它们到Background元素中。这种做法有两个优势:
1. 如果你需要改变这些步骤,你只需要在一个地方改变他们;
2. 这些步骤隐藏到background中,当你遇到每个单独的场景时,你可以仅仅关注当前场景的惟一性和重要性。
查看一个已存在的场景,仅仅使用基本元素,然后使用background来重构它,从而该进它的可读性。下面是改进前的feature:
Feature:Change PIN
Customers being issued new cards are supplied with a Personal Identification Number (PIN) that is randomly generated by the system.
In order to be able to change it to something they can easily remember, customers with new bank cards need to be able to change their PIN using the ATM.
Scenario:Change PIN successfully
Given I have been issued a new card
And I insert the card, entering the correct PIN
When I choose "Change PIN" from the menu
And I change the PIN to 9876
Then the system should remember my PIN is now 9876
Scenario:Try to change PIN to the same as before
Given I have been issued a new card
And I insert the card, entering the correct PIN
When I choose "Change PIN" from the menu
And I try to change the PIN to the original PIN number
Then I should see a warning message
And the system should not have changed my PIN
你可以看到这里有两个场景,但是如果不仔细阅读,很难看懂每个场景的每一步。每个场景的前三步,用来定义场景的上下文,在两个场景中完全重复。这个重复使得每个场景的本质意义很难看清。
然后将三句step移动到background中,像下面:
Feature:Change PIN
As soon as the bank issues new cards to customers, they are supplied with a Personal Identification Number (PIN) that is randomly generated by the system.
In order to be able to change it to something they can easily remember, customers with new bank cards need to be able tochange their PIN using the ATM.
Background:
GivenI have been issued a new card
AndI insert the card, entering the correct PIN
And I choose "Change PIN" from the menu
Scenario: Change PIN successfully
WhenI change the PIN to 9876
Thenthe system should remember my PIN is now 9876
Scenario: Try to change PIN to the same as before
When I try to change the PIN to the original PIN number
Then I should see a warning message
Andthe system should not have changed my PIN
我们的重构完全没有改变测试的行为:在运行时,background中的step在每个scenario开始前都会执行。我们要做的是使得每个独立的场景非常易读。
可以在每个feature中拥有一个background,他必须出现在任何Scenario或者Scenario Outline元素之前。就像所有其他的Gherkin元素,你可以给background一个名称,并且你有空间来设置多行描述,然后编写step。例如:
Feature:Change PIN
In order to be able to change it to something they can easily remember, customers with new bank cards need to be able to change their PIN using the ATM.
Background:Insert a newly issued card and sign in Whenever the bank issues new cards to customers, they are supplied with a Personal Identification Number (PIN) that is randomly generated by the system.
Given I have been issued a new card
And I insert the card, entering the correct PIN
...
Background并不是一直必要的,但它经常能够通过移除单独场景中的重复step,改进feature的可读性。这里有一些提示:
1. 不要使用background设置复杂状态,除非状态是必须要阅读者知道。例如我们没有提及系统生成的PIN真实数值,因为这个细节和所有的场景都不想管。
2. 保持你的background节简短。毕竟,我们期望用户阅读你的场景时,真正记住background。如果background超过4行,你能不能找到一种方法仅仅使用2步来表达这个行为?
3. 使得Background节生动、形象。使用带颜色的名字,尽力描述一个故事,因为比起记住一些愚钝的名字,如UserA、UserB、Site1等,你的阅读者记住故事容易得多。如果这个值得一提,使它真正地突出出来。
4. 保持你的场景简短,并且不要包含太多。如果background超过3或4行step,考虑使用更高级的steps或者把他们分到两个文件中。如果一个feature太长,你可以使用background作为一个很好的指示器:如果新场景不适合已存在的background,考虑分割feature。
5. 避免技术细节,例如清空队列、开启后端服务、打开浏览器等。大多数事情已经被读者假定,并且这里有很多方法将这些行为移动到你的支持代码中,后面我会解释。
Background对Given steps是很有用的,因为这些step经常重复出现在每个场景中,应该移动到一个地方。这可以帮助保持你的场景清洁和简洁。
5.2 Data Tables
有时场景中的step需要描述数据,并且这些数据不容易匹配在一行Given,When,Then step中。Gherkin允许我们将这些细节放置到step下的一个表格中。Data table帮助你扩展Gherkin step,能够让你一行来包含大量数据。例如,考虑下面的step:
重构到background
重构是改进代码的过程,提高代码的可读性或者设计而不改变代码的行为。这个技术应用到Gherkin feature,也会应用到你的代码库的其他部分。当你理解你的领域逐渐增长时,你要在项目中对更新你的feature来有反应。
经常地,你不会立即看到background。你一般会通过写一个或两个场景开始,并且当你编写到第三个时,你会注意到一些相同的步骤。当你发现一个feature中有一些相同或类似的step重复出现在某些场景中,查看你是否能重构来提取这些步骤到一个background。应该对这个给予鼓励,因为尽管存在可能造成问题、破坏一些事情的风险,但是这是非常好的重构。一旦你做了,feature必然会和从前做相同的事情,但是更易读了。
Given a User"Michael Jackson" born on August 29, 1958
And a User "Elvis" born on January 8, 1935
Anda User "John Lennon" born on October 9, 1940
...
非常烦躁。在传统的说明文档中我们都不能容忍这种重复的东西,当然我们不能在Cucumber说明文档中容忍了。我们可以收集这些步骤成一个单独的step,可以使用一个表格来表达数据:
Given these Users:
| name| date of birth|
| Michael Jackson | August 29, 1958 |
| Elvis| January 8, 1935 |
| John Lennon| October 9, 1940 |
这是非常清晰的。这个表格从紧邻的step开始,然后它的数据使用管道字符|来分割。你可以使用空白字符让表格更加整洁,尽管cucumber不关心你做的事;它会取走每个单元所有的值,忽略周围的空白。
在上一个表格中,我们为每一列使用了一个标题,但是那仅仅是使它对特殊step有意义。你有权利以其他的方式指定数据,例如将表头放在最下面:
Then I should see a vehicle that matches the following description:
| Wheels| 2|
| Max Speed| 60 mph|
| Accessories | lights, shopping basket |
或者仅仅指定列表:
Then my shopping list should contain:
| Onions|
| Potatoes |
| Sausages |
| Apples|
| Relish|
要解释如何与这些不同表格一起工作,我们需要转到step定义层去。
在step定义中使用DataTable
我们使用一个tic-tac-toe游戏来解释如何使用data table。假如我们正在构建一个tic-tac-toe游戏,我们开始处理基本feature,在板上移动。我们如下启动场景:
Feature:
Scenario:
Given a board like this:
| | 1 | 2 | 3 |
| 1 | | | |
| 2 | | | |
| 3 | | | |
When player x plays in row 2, column 1
Then the board should look like this:
| | 1 | 2 | 3 |
| 1 | | | |
| 2 | x | | |
| 3 | | | |
我们向你展示如何获取第一步的表格,然后展示如何操作它,最后比较期望的面板和真实的面板。
运行cucumber来生成step定义片段,粘贴到features/step_definitions/board_steps.rb:
Given /â board like this:$/ do |table|
# table is a Cucumber::Ast::Table
pending # express the regexp above with the code you wish you had
end
When /^player x plays in row (\d+), column (\d+)$/ do |arg1, arg2|
pending # express the regexp above with the code you wish you had
end
Then /^the board should look like this:$/ do |table|
# table is a Cucumber::Ast::Table
pending # express the regexp above with the code you wish you had
end
注意到两个接收到表格的step有点不同。注释告诉你传进来的参数中有个Cucumber::Ast::Table对象。这是一个丰富的对象,拥有大量的交互方法。现在为你展示最常用的一些。
让我们来充实这些step定义
将表格转换成数组
其实,表格就是一个2维数组。通常我们想要它的raw形式来处理它,因此我们调用raw方法就可以获得它的raw形式。让我们从table中获得raw数据,并且将它存储到一个实例变量@board中,在第二个步骤中当我们要做一个移动时,我们就可以操作它。
作为一个实验,为第二个step定义添加一个实现,打印出raw表格。
Given /â board like this:$/ do |table|
@board = table.raw
end
When /^player x plays in row (\d+), column (\d+)$/ do |row, col|
puts @board
pending # express the regexp above with the code you wish you had
end
Then /^the board should look like this:$/ do |table|
# table is a Cucumber::Ast::Table
pending # express the regexp above with the code you wish you had
end
当你运行cucumber,你会看到二维数组。(ruby1.8.7上结果是不一样的,看不到数组的样子,仅仅打印所有的值)。
$ cucumber -f progress
.
[["", "1", "2", "3"], ["1", "", "", ""], ["2", "", "", ""], ["3", "", "", ""]]
P-
(::) pending steps (::)
features/tic_tac_toe.feature:8:in ‘When player x plays in row 2, column 1’
1 scenario (1 pending)
3 steps (1 skipped, 1 pending, 1 passed)
0m0.008s
使用Diff方式比较表格
现在可以启动一个失败的测试,现在我们会跳过任何对表格的操作。因此,删除第二个step定义的主体,然后在最后一个step定义中做如下实现:
Given /â board like this:$/do |table|
@board = table.raw
end
When /^player x plays in row (\d+), column (\d+)$/do |row, col|
end
Then /^the board should look like this:$/do |expected_table|
expected_table.diff!(@board)
end
我们在table上使用diff!方法,正如我们看到的table传送了真实的表格。当你运行cucumber,你会看到step会失败,因为这些表格不相同。
$ cucumber
Feature:
Scenario:
Given a board like this:
| | 1 | 2 | 3 |
| 1 | | | |
| 2 | | | |
| 3 | | | |
When player x plays in row 2, column 1
Then the board should look like this:
| | 1 | 2 | 3 |
| 1 | | | |
| 2 | x | | |
| 3 | | | |
Tables were not identical (Cucumber::Ast::Table::Different)
./features/step_definitions/board_steps.rb:9
features/tic_tac_toe.feature:9
Failing Scenarios:
cucumber features/tic_tac_toe.feature:2
1 scenario (1 failed)
3 steps (1 failed, 2 passed)
0m0.012s
如果你的控制台输出有颜色,你会看到相同的部分是绿色的。期望的单元格但没有找到的是黄色的,下一行真实的单元格打印出来是灰色的。可以通过传进一个hash选项到diff!进行比较行为,你可以从文档中找到关于Cucumber::Ast::Table的相关信息。
修改When step使得场景通过。添加下面的实现到第二个step定义中:
When /^player x plays in row (\d+), column (\d+)$/do |row, col|
row, col = row.to_i, col.to_i
@board[row][col] = 'x'
end
运行场景,场景会通过。
这只是cucumber中数据表的一次体验。我们鼓励你阅读cucumber::Ast::Table文档,与它进行一些游戏。
Data tables是Gherkin的一个重要特性。他们真的是万能的,并且他们帮助你简洁的表达数据,就像你写在一个正常说明文档中。使用background和data table,你可以大量地减少场景中的噪音和混乱。甚至当你使用这些工具时,你会看到一个模式,另一个场景看起来和它非常像。这就是scenario outline可以帮助你了。
5.3 Scenario Outline
有时,一些场景拥有实际上相同的步骤,仅仅只有输入数据或者输出数据不同。例如,假定我们正在测试ATM的固定取款功能按钮:
Feature:Withdraw Fixed Amount
The "Withdraw Cash" menu contains several fixed amounts tospeed up transactions for users.
Scenario: Withdraw fixed amount of $50
Given I have $500 in my account
When I choose to withdraw the fixed amount of $50
Then I should receive $50 cash
And the balance of my account should be $450
Scenario: Withdraw fixed amount of $100
Given I have $500 in my account
When I choose to withdraw the fixed amount of $100
Then I should receive $100 cash
And the balance of my account should be $400
Scenario: Withdraw fixed amount of $200
Given I have $500 in my account
When I choose to withdraw the fixed amount of $200
Then I should receive $200 cash
And the balance of my account should be $300
这些重复的语句使得feature非常枯燥。这样很难让人看到每个场景的本质,我们可以使用Scenario outline来一次性定义所有场景,只需要传给它取值的列表。下面是使用scenario outline重构后的场景:
Feature:Withdraw Fixed Amount
The "Withdraw Cash" menu contains several fixed amounts tospeed up transactions for users.
Scenario Outline: Withdraw fixed amount
Given I have <Balance> in my account
When I choose to withdraw the fixed amount of <Withdrawal>
Then I should receive <Received> cash
And the balance of my account should be <Remaining>
Examples:
| Balance | Withdrawal | Received | Remaining |
| $500 | $50 | $50 | $450 |
| $500 | $100 | $100 | $400 |
| $500 | $200 | $200 | $300 |
我们在scenario outline中使用占位符,占位符用尖角表示,我们会使用真实的内容来替换尖角内的内容。Scenario outline必须有Examples表格才有意思。Examples表格包含多行为每个占位符提供了替代的值。
在一个feature中,你可以有任意数量的Scenario outline元素,每个Scenario Outline后面可以有任意的Example表格。在执行这个feature时,Cucumber会将表格的每一行转化为一个场景。你可以使用—expand参数证明这个观点,--expand选项会打印scenario outline中的每一个例子:
$Cucumber –expand
使用scenario outline的一个优势是:你可以很清晰地看到例子间的空隙。在我们的例子中,还没有任何的边界测试。例如当你想要比你余额更多的取款。当你看到这些值列在一个表格中时,这就会变得更加明显。
记住尽管在Gherkin中编写它们的语法是相同的,这些表格和数据表格是完全不一样的。数据表格仅仅附加在一个单独的step上,描述一块数据。在scenario outline中,Examples中的每一行表示一个完整的场景,都可以在cucumber中执行。事实上,如果你想要它更加可读,你可以使用关键字Scenarios来代替Examples。
更大的占位符
可以想象,scenario outline占位符只能用在step的某些数据上。事实上,当cucumber编译Examples表格时,它不管占位符在哪。因此,你可以替换任意多的任何step的文本。
让我们用测试边界条件的例子来演示:我们需要取出比余额更多地数额的情况。我们应该做什么呢?尽可能地给他钱,还是给他显示一个错误信息?我们询问利益相关者后,他们都会认为应该显示一个错误信息。
Scenario:Try to withdraw too much
Given I have $100 in my account
When I choose to withdraw the fixed amount of $200
Then I should see an error message
And the balance of my account should be $100
这里有大量的重复代码,但是因为Then step差异很大,我们不能将这个添加到scenario outline中。可以吗?
我们可以修改scenario outline,替换<Received>占位符改成更加抽象的<outcome>:
Scenario Outline:Withdraw fixed amount
Given I have <Balance> in my account
When I choose to withdraw the fixed amount of <Withdrawal>
Then I should <Outcome>
And the balance of my account should be <Remaining>
Examples:
| Balance | Withdrawal | Remaining | Outcome |
| $500 | $50 | $450 | receive $50 cash |
| $500 | $100 | $400 | receive $100 cash |
| $500 | $200 | $300 | receive $200 cash |
现在我们可以很简单地添加我们的失败例子到表格的底部了。
Scenario Outline:Withdraw fixed amount
Given I have <Balance> in my account
When I choose to withdraw the fixed amount of <Withdrawal>
Then I should <Outcome>
And the balance of my account should be <Remaining>
Examples:
| Balance | Withdrawal | Remaining | Outcome |
| $500 | $50 | $450 | receive $50 cash |
| $500 | $100 | $400 | receive $100 cash |
| $500 | $200 | $300 | receive $200 cash |
| $100 | $200 | $100 | see an error message |
我们使用一个占位符莱代替step中的任意文本。注意不管占位符出现的先后顺序,cucumber可以正确匹配。
尽管这是一个有用的技术,小心程序员不顾一切的消灭重复。如果你移动太多的文本到Examples表格中,阅读场景的流程会变得很困难。记住你的目标是可读性,因此不要太多地使用这个技术,通过其他人的定期阅读和反馈,经常运行这些feature文件。
多个Examples表格
Cucumber可以处理Scenario Outline中任意数量的Examples元素,意味着你可以将不同类型的例子分组。例如:
Scenario Outline:Withdraw fixed amount
Given I have <Balance> in my account
When I choose to withdraw the fixed amount of <Withdrawal>
Then I should <Outcome>
And the balance of my account should be <Remaining>
Examples: Successful withdrawal
| Balance | Withdrawal | Outcome | Remaining |
| $500 | $50 | receive $50 cash | $450 |
| $500 | $100 | receive $100 cash | $400 |
Examples: Attempt to withdraw too much
| Balance | Withdrawal | Outcome | Remaining |
| $100 | $200 | see an error message | $100 |
| $0 | $50 | see an error message | $0 |
通常,你可以为每个Examples表格一个名称和描述。当你有一个很大的example集合时,将他们分割到多个表格中,可以很容易被人理解。