目录

上一章我们选择了第一次迭代的用户故事,并使用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。

测试替身

模拟被测对象的技术成为测试替身。你可能熟悉stubsmocks,这都是测试替身技术。这方面,在第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技术驱动具体对象的编写。