目录

欢迎回来!我们之前的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驱动内部结构的改造,逐步完成代码开发。