第4章 Cucumber自动化测试 《RSpec手册》
上一章我们选择了第一次迭代的用户故事,并使用Cucumber书写了功能和场景。现在我们开始编写可执行的代码。
这时,功能文件应该在features
目录中,并使用.feature
为扩展名,叫做功能文件。现在有两个这样的功能文件。
1: features/codebreakerstartsgame.feature
#languague: zh-CN
功能: 破译者开始玩游戏
作为一个破译者
我要开始游戏
去破译密码
场景: 开始游戏
假如我还没开始
当我开始新的游戏
那么我应当看到"欢迎来玩破译密码的游戏!"这样的欢迎辞
并且我应当看到"请从1-6的数字中选择4个作为猜想答案:"的提示
2: features/codebreakersubmitesguess.feature
#language: zh-CN
功能: 破译者提交猜想
破译者从1-6中选择四个数字作为猜想答案。
游戏会反馈猜想结果:+和-。
答案中的每个数字若在密码中存在,并且位置准确就返回一个+;若仅存在数字但位置不准确就返回一个-。
场景大纲: 猜想匹配测试
假如密码是"<code>"
当我提交"<guess>"
那么我应当得到的反馈是"<mark>"
例子: 没有匹配
| code | guess | mark |
| 1234 | 5555 | |
例子: 1个数字正确
| code | guess | mark |
| 1234 | 1555 | + |
| 1234 | 2555 | - |
例子: 2个数字正确
| code | guess | mark |
| 1234 | 5254 | ++ |
| 1234 | 5154 | +- |
| 1234 | 2545 | -- |
例子: 3个数字正确
| code | guess | mark |
| 1234 | 5234 | +++ |
| 1234 | 5134 | ++- |
| 1234 | 5124 | +-- |
| 1234 | 5123 | --- |
例子: 全部正确
| code | guess | mark |
| 1234 | 1234 | ++++ |
| 1234 | 1243 | ++-- |
| 1234 | 1423 | +--- |
| 1234 | 4321 | ---- |
我们需要在features/support
目录中保存一个env.rb
文件,.rb
扩展名告诉Cucumber我们要使用Ruby。
如果你没有在前一章中运行功能文件,现在可以试试。在目录codebreaker
中打开shell,并运行cucumber,你应该看到一堆输出,里面包含了对.feature
文件的分析和工作进度处理。
当运行cucumber命令时,你应该看到类似下面的输出(他们是黄色字体):
假如(/^我还没开始$/) do
pending # express the regexp above with the code you wish you had
end
我们根据这些提示创建步骤定义文件features/step_definitions/codebreaker_steps.rb
#encoding: utf-8
假如(/^我还没开始$/) do
end
当(/^我开始新的游戏$/) do
Codebreaker::Game.new.start
end
那么(/^我应当看到"(.*?)"这样的欢迎辞$/) do |arg1|
pending # express the regexp above with the code you wish you had
end
那么(/^我应当看到"(.*?)"的提示$/) do |arg1|
pending # express the regexp above with the code you wish you had
end
假如(/^密码是"(.*?)"$/) do |arg1|
pending # express the regexp above with the code you wish you had
end
当(/^我提交"(.*?)"$/) do |arg1|
pending # express the regexp above with the code you wish you had
end
那么(/^我应当得到的反馈是"(.*?)"$/) do |arg1|
pending # express the regexp above with the code you wish you had
end
现在运行cucumber,输出结果中包含应一段红色的内容,说明此处无法通过测试,类似这样:
当我开始新的游戏 # features/step_definitions/codebreaker_steps.rb:6
uninitialized constant Codebreaker (NameError)
./features/step_definitions/codebreaker_steps.rb:7:in `/^我开始新的游戏$/'
features/codebreaker_starts_game.feature:10:in `当我开始新的游戏'
那么我们开始在features
目录并列的位置,建立一个lib
目录,用来组织应用程序的代码文件。现在增加lib/codebreaker/game.rb
文件:
#encoding: utf-8
module Codebreaker
class Game
def start
end
end
end
再增加一个文件lib/codebreaker.rb
:
require 'codebreaker/game'
最后,编辑环境设置文件features/support/env.rb
:
$LOAD_PATH << File.expand_path('../../../lib', __FILE__)
#require 'codebreaker'
再次运行cucumber,结果应该是:
16 scenarios (15 pending, 1 passed)
49 steps (29 skipped, 15 pending, 5 passed)
Cucumber默认会加载features/support/env.rb
文件,并根据指引,加载到了codebreaker/game.rb
文件,而这个文件中定义了Codebreaker
模块以及附带一个start()
空方法的Game
类。其实,这时还可以直接运行指定的功能文件:
$ cucumber features/codebreaker_starts_game.feature
#language: zh-CN
功能: 破译者开始玩游戏
作为一个破译者
我要开始游戏
去破译密码
场景: 开始游戏 # features/codebreaker_starts_game.feature:8
假如我还没开始 # features/step_definitions/codebreaker_steps.rb:3
当我开始新的游戏 # features/step_definitions/codebreaker_steps.rb:6
那么我应当看到"欢迎来玩破译密码的游戏!"这样的欢迎辞 # features/step_definitions/codebreaker_steps.rb:10
TODO (Cucumber::Pending)
./features/step_definitions/codebreaker_steps.rb:11:in `/^我应当看到"(.*?)"这样的欢迎辞$/'
features/codebreaker_starts_game.feature:11:in `那么我应当看到"欢迎来玩破译密码的游戏!"这样的欢迎辞'
而且我应当看到"请从1-6的数字中选择4个作为猜想答案:"的提示 # features/step_definitions/codebreaker_steps.rb:14
1 scenario (1 pending)
4 steps (1 skipped, 1 pending, 2 passed)
0m0.003s
第二步通过以后,我们就进入那么的步骤测试。这时一个带有正则表达式的步骤捕获,写需求时假定了在这一步的消息是显示在标准输出上(STDOUT)。当然,我们在控制台中运行Cucumber,当然是使用STDOUT。我们的真实目的只是捕获消息,所以我们需要一个假的对象,让所设计的游戏认为它就是STDOUT。
测试替身
模拟被测对象的技术成为测试替身。你可能熟悉stubs或mocks,这都是测试替身技术。这方面,在第14章有更深入的介绍。
下面是那么部分的测试代码改进后的写法:
当(/^我开始新的游戏$/) do
game = Codebreaker::Game.new(output)
game.start
end
此时可以运行一下cucumber,提示应该是初始化Game对象时的参数错误
,所以接着修改Game
对象:
module Codebreaker
class Game
def initialize(output)
end
def start
end
end
end
再次运行cucumber,提示变为:
expected [] to include "欢迎来玩破译密码的游戏!" (RSpec::Expectations::ExpectationNotMetError)
我们已经使用Cucumber从应用行为上驱动了应用框架的编写,根据BDD循环
的思路,至此可以告一段落,接着使用RSpec技术驱动具体对象的编写。