第5章 使用RSpec描述代码 《RSpec手册》
在前面的章节中,我们介绍了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个作为猜想答案:
这就是我们在第一阶段的迭代成果!