目录

在前面的章节中,我们介绍了Cucumber,并使用它描述了密码破译这个游戏应用的行为,这是从应用层,从外部做的。我们为Cucumber功能文件书写了测试步骤的代码定义,然后遗留了一个失败的步骤:

expected [] to include "欢迎来玩破译密码的游戏!" (RSpec::Expectations::ExpectationNotMetError)

本章我们将使用RSpec从应用的内部描述应用行为:即Game类的实例行为。

开始使用RSpec

首先创建一个spec目录,并包含一个codebreaker的子目录。现在我们创建一个game_spec.rb的文件。在整个过程中,我们会与源文件保持同步,例如存在一个lib/codebreaker/game.rb的源文件,就会建立一个spec/codebreaker/game_spec.rb的测试文件,这种做法是使用RSpec的一种非技术规范。下面是game_spec.rb的内容:

#encoding: utf-8
require 'spec_helper'

module Codebreaker
  describe Game do
    describe "#start" do
      it "发出欢迎消息"
      it "游戏提示"
    end
  end
end

spec/spec_helper.rb的内容如下:

require 'codebreaker'

这个与使用Cucumber时需要的env.rb文件有点类似。

最后,在项目根目录下配置一个.spec的文件(具体参数可以查询rspec --help来配置),这样就可以获得更漂亮的输出:

$ cat .rspec 
--format d
--color

Red:从一个失败的RSpec测试开始

现在可以运行rspec了:

$ rspec spec/codebreaker/game_spec.rb 

Codebreaker::Game
  #start
    发出欢迎消息 (PENDING: Not yet implemented)
    游戏提示 (PENDING: Not yet implemented)

Pending:
  Codebreaker::Game#start 发出欢迎消息
    # Not yet implemented
    # ./spec/codebreaker/game_spec.rb:7
  Codebreaker::Game#start 游戏提示
    # Not yet implemented
    # ./spec/codebreaker/game_spec.rb:8

Finished in 0.00054 seconds
2 examples, 0 failures, 2 pending

我们来实现rspec测试步骤:

describe "#start" do
  it "发出欢迎消息" do
    output = double('output')
    game = Game.new(output)
    output.should_receive(:puts).with('欢迎来玩破译密码的游戏!')
    game.start
  end

  it "游戏提示"
end

Green:让RSpec实例通过

然后修改一下lib/codebreaker/game.rb文件:

class Game
  def initialize(output)
    @output = output
  end
  def start
    @output.puts '欢迎来玩破译密码的游戏!'
  end
end

重新运行rspec spec/codebreaker/game_spec.rb

$ rspec spec/codebreaker/game_spec.rb 

Codebreaker::Game
  #start
    发出欢迎消息
    游戏提示 (PENDING: Not yet implemented)

Pending:
  Codebreaker::Game#start 游戏提示
    # Not yet implemented
    # ./spec/codebreaker/game_spec.rb:14

Finished in 0.00086 seconds
2 examples, 0 failures, 1 pending

此时重新运行一下Cucumber:

$ cucumber features/codebreaker_starts_game.feature 
#language: zh-CN
功能: 破译者开始玩游戏

  作为一个破译者
  我要开始游戏
  去破译密码

  场景: 开始游戏                           # features/codebreaker_starts_game.feature:8
    假如我还没开始                          # features/step_definitions/codebreaker_steps.rb:17
    当我开始新的游戏                         # features/step_definitions/codebreaker_steps.rb:20
    那么我应当看到"欢迎来玩破译密码的游戏!"这样的欢迎辞      # features/step_definitions/codebreaker_steps.rb:25
    而且我应当看到"请从1-6的数字中选择4个作为猜想答案:"的提示 # features/step_definitions/codebreaker_steps.rb:29
      TODO (Cucumber::Pending)
      ./features/step_definitions/codebreaker_steps.rb:30:in `/^我应当看到"(.*?)"的提示$/'
      features/codebreaker_starts_game.feature:12:in `而且我应当看到"请从1-6的数字中选择4个作为猜想答案:"的提示'

1 scenario (1 pending)
4 steps (1 pending, 3 passed)
0m0.003s

原来无法通过的cucumber场景,现在通过了!!

下面要处理“游戏提示”的问题,首先修改game.rb文件:

def start
  @output.puts '欢迎来玩破译密码的游戏!'
  @output.puts '请从1-6的数字中选择4个作为猜想答案:'
end

再修改game_spec.rb文件:

it "发出欢迎消息" do
  output = double('output').as_null_object
  game = Game.new(output)
  output.should_receive(:puts).with('欢迎来玩破译密码的游戏!')
  game.start
end
it "游戏提示" do
  output = double('output').as_null_object
  game = Game.new(output)
  output.should_receive(:puts).with('请从1-6的数字中选择4个作为猜想答案:')
  game.start
end

请注意,.as_null_object这一句很关键,否则两个实例都会失败。

$ rspec spec/codebreaker/game_spec.rb 

Codebreaker::Game
  #start
    发出欢迎消息
    游戏提示

Finished in 0.00113 seconds
2 examples, 0 failures

Refactor重构

Martin Foler所说的,“重构是一种优化软件的过程,目的是优化系统内部结构,但不会改变外部行为”。

怎么能确保在重构时不改变系统原有的行为呢?答案是频繁测试,通过了就代表行为没有改变。如果无法通过,就应该在最后的代码变动中寻找原因,并快速解决问题。

下面使用before(:each)技术来重构RSpec测试部分:

describe "#start" do
  before(:each) do
    @output = double('output').as_null_object
    @game = Game.new(@output)
  end
  it "发出欢迎消息" do
    @output.should_receive(:puts).with('欢迎来玩破译密码的游戏!')
    @game.start
  end

  it "游戏提示" do
    @output.should_receive(:puts).with('请从1-6的数字中选择4个作为猜想答案:')
    @game.start
  end
end

另外一种重构的技巧是使用let

describe "#start" do
  let(:output){ double('output').as_null_object }
  let(:game){ Game.new(output) }

  it "发出欢迎消息" do
    output.should_receive(:puts).with('欢迎来玩破译密码的游戏!')
    game.start
  end

  it "游戏提示" do
    output.should_receive(:puts).with('请从1-6的数字中选择4个作为猜想答案:')
    game.start
  end
end

无论如何,运行rspec之后应该看到如下结果:

$ cucumber features/codebreaker_starts_game.feature 
#language: zh-CN
功能: 破译者开始玩游戏

  作为一个破译者
  我要开始游戏
  去破译密码

  场景: 开始游戏                           # features/codebreaker_starts_game.feature:8
    假如我还没开始                          # features/step_definitions/codebreaker_steps.rb:17
    当我开始新的游戏                         # features/step_definitions/codebreaker_steps.rb:20
    那么我应当看到"欢迎来玩破译密码的游戏!"这样的欢迎辞      # features/step_definitions/codebreaker_steps.rb:25
    而且我应当看到"请从1-6的数字中选择4个作为猜想答案:"的提示 # features/step_definitions/codebreaker_steps.rb:29

1 scenario (1 passed)
4 steps (4 passed)
0m0.003s
homewaytekiMacBook-Pro:1 homeway$ rspec spec/codebreaker/game_spec.rb 

Codebreaker::Game
  #start
    发出欢迎消息
    游戏提示

Finished in 0.00113 seconds
2 examples, 0 failures

现在对Cucumber功能文件也稍作整理,然后运行cucumber

$ cucumber features/codebreaker_starts_game.feature 
#language: zh-CN
功能: 破译者开始玩游戏

  作为一个破译者
  我要开始游戏
  去破译密码

  场景: 开始游戏                           # features/codebreaker_starts_game.feature:8
    假如我还没开始                          # features/step_definitions/codebreaker_steps.rb:17
    当我开始新的游戏                         # features/step_definitions/codebreaker_steps.rb:20
    那么我应当看到"欢迎来玩破译密码的游戏!"这样的欢迎辞      # features/step_definitions/codebreaker_steps.rb:25
    而且我应当看到"请从1-6的数字中选择4个作为猜想答案:"的提示 # features/step_definitions/codebreaker_steps.rb:29

1 scenario (1 passed)
4 steps (4 passed)
0m0.004s

很好,看样子测试都通过了!

那么,我们可以将行为驱动的开发成果用起来。

新建一个脚本文件bin/codebreaker

#!/usr/bin/env ruby
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'codebreaker'

game = Codebreaker::Game.new(STDOUT)
game.start

运行一下:

$ bin/codebreaker 
欢迎来玩破译密码的游戏!
请从1-6的数字中选择4个作为猜想答案:

这就是我们在第一阶段的迭代成果!