第6章 添加新功能 《RSpec手册》
欢迎回来!我们之前的Codebreaker游戏进行到命令行开启后的提示,接下来的功能是开始提交猜测答案。
在猜测答案的功能中我们要介绍一下算法。这很有趣,因为算法要覆盖非常多的可能性。这里可没有之前那么简单,我们需要大量的场景和实例。很幸运,我们在RSpec和Cucumber中都有好用的工具,让我们尽量不去做重复的事情。
Cucumber中的场景轮廓
这里是我们在第三章中写的Cucumber功能描述,是第一次的迭代计划。
#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 | ---- |
功能描述已经很清晰,不需要解释了。后半部分是场景大纲,以行数据的形式提供了若干实例。而场景描述中使用了尖括号作为占位符,对应到例子中的数据字段。
我们运行下面的命令:
$ cucumber features/codebreaker_submits_guess.feature
与之前看到的类似,如果还没有相应的步骤定义,输出上会显示可供粘贴的步骤定义框架。
步骤定义
场景轮廓和例子数据表的步骤定义,与我们之前学过的类似,我们仍然使用正则表达式来步骤数据和block代码块。Cucumber的自动化功能会将每个例子拆分成独立的实际场景。
下面修改步骤定义:
假如(/^密码是"(.*?)"$/) do |secret|
game = Codebreaker::Game.new(output)
game.start(secret)
end
正则表达式会捕获括号中的字符串组。被捕获的密码(例子中是"1234")会传递给上面的步骤定义。这些代码应该看起来很熟悉,与之前的步骤定义很相似。
现在再次欲行Cucumber,应该看到包含类似内容的输出:
例子: 没有匹配
| code | guess | mark |
| 1234 | 5555 | |
wrong number of arguments (1 for 0) (ArgumentError)
./lib/codebreaker/game.rb:8:in `start'
每个场景都应该包含类似错误提示。这是个好消息,因为错误提示告诉我们下一步该做什么:让Game.start()方法接受参数。
响应变化
此时,所有RSpec代码已经通过测试,但Cucumber场景报错了。这就是从外部行为驱动内部开发的过程。
我们新的步骤定义要求Game.start()接受一个secret参数,但RSpec实例中的start()不接受任何参数。此时直接让start()接受一个参数,则RSpec肯定会报错,不过让它有个默认值就可以过关了:
def start(secret = nil)
@output.puts '欢迎来玩破译密码的游戏!'
@output.puts '请从1-6的数字中选择4个作为猜想答案:'
end
运行specs,应该还是可以通过的。现在运行*$ cucumber features/codebreakersubmitsguess.feature *,应该看到这样的输出:
14 scenarios (14 undefined)
42 steps (28 undefined, 14 passed)
0m0.032s
此时,场景执行结果只有通过的和未定义的,但并没有失败的,并且rspec是通过的。现在我们修改rspec并传递一个密码到start(),像这样:
it "发出欢迎消息" do
output.should_receive(:puts).with('欢迎来玩破译密码的游戏!')
game.start('1234')
end
it "游戏提示" do
output.should_receive(:puts).with('请从1-6的数字中选择4个作为猜想答案:')
game.start('1234')
end
运行rspec,应该还是通过的。现在移除start()方法中的默认值:
def start(secret)
@output.puts '欢迎来玩破译密码的游戏!'
@output.puts '请从1-6的数字中选择4个作为猜想答案:'
end
依次运行看看,场景codebreakersubmitsguess scenarios还是通过的,rspec也是通过的,但codebreakerstartsgame.feature失败了。
wrong number of arguments (0 for 1) (ArgumentError)
./lib/codebreaker/game.rb:8:in `start'
./features/step_definitions/codebreaker_steps.rb:22:in `/^我开始新的游戏$/'
已经提示问题的位置了,步骤定义文件第22行!修改一下,传递一个值"1234"给它吧:
当(/^我开始新的游戏$/) do
game = Codebreaker::Game.new(output)
game.start '1234'
end
现在spec都通过了,并且场景只有未定义和通过的。
场景轮廓的魔术
请记住,每个例子都会根据场景轮廓生成独立场景。下面开始从提示中拷贝文本,粘贴到步骤定义中:
当(/^我提交"(.*?)"$/) do |arg1|
pending # express the regexp above with the code you wish you had
end
然后修改步骤定义代码:
当(/^我提交"(.*?)"$/) do |guess|
@game.guess(guess)
end
这里需要一个实例变量@game,所以还要调整条件步骤中的代码:
假如(/^密码是"(.*?)"$/) do |secret|
@game = Codebreaker::Game.new(output)
@game.start(secret)
end
此时运行cucumber,会看到包含下面内容的提示:
例子: 没有匹配
| code | guess | mark |
| 1234 | 5555 | |
undefined method `guess' for #<Codebreaker::Game:0x007fa453b00aa0> (NoMethodError)
很清楚,cucumber告诉我们缺少guess()方法定义。那么,我们就增加这个方法到Game类中:
class Game
def initialize(output)
@output = output
end
def start(secret)
@output.puts '欢迎来玩破译密码的游戏!'
@output.puts '请从1-6的数字中选择4个作为猜想答案:'
end
def guess(guess)
end
end
重新运行cucumber,得到如下结果:
14 scenarios (14 undefined)
42 steps (14 undefined, 28 passed)
0m0.032s
没有错误,之前未通过的部分通过了,只剩下未定义部分。我们继续增加步骤定义:
那么(/^我应当得到的反馈是"(.*?)"$/) do |mark|
output.messages.should include(mark)
end
运行cucumber,得到如下错误提示:
例子: 没有匹配
| code | guess | mark |
| 1234 | 5555 | |
expected ["欢迎来玩破译密码的游戏!", "请从1-6的数字中选择4个作为猜想答案:"] to include "" (RSpec::Expectations::ExpectationNotMetError)
提示中说,反馈的mark与实例不匹配。
至此,我们已经完成了一份可执行的Cucumber行为定义,执行结果是未能通过。如何通过的问题将取决于系统内部的改造。因此,在下一章我们会使用RSpec驱动内部结构的改造,逐步完成代码开发。