我正试图让我的头脑围绕测试驱动的设计,特别是RSpec。但我遇到了RSpec Book的一些例子。
在本书中,我们测试$ STDOUT的输出如下:
output = double('output')
game = Game.new
output.should_receive(:puts).with('Welcome to Codebreaker!')
game.start()
嗯,这是一种时尚之后的作用。但是,为什么我应该关心Game对象是否使用puts()方法?如果我把它改成print(),它真的会破坏测试吗?并且,更重要的是,这不是针对TDD的主要内容之一 - 我应该测试该方法的作用(设计)而不是它是如何做的(实现)?
有没有什么方法可以编写一个测试,只测试最终在$ STDOUT上的内容,而不用看什么方法把它放在那里?
答案 0 :(得分:8)
创建一个能够写出状态的显示类。
您的生产代码将使用此显示对象,因此您可以自由更改写入STDOUT的方式。当你的测试依赖于抽象时,这个逻辑只有一个地方。
例如:
output = stub('output')
game = Game.new(output)
output.should_receive(:display).with('Welcome to Codebreaker!')
game.start()
虽然您的生产代码会包含诸如
之类的内容class Output
def display(message)
# puts or whatever internally used here. You only need to change this here.
end
end
我通过执行以下操作来完成此测试:
def start
@output.display('Welcome to Codebreaker!')
end
这里的生产代码并不关心输出的显示方式。它可以是任何形式的显示,现在抽象已经到位。
以上所有理论都与语言无关,并且是一种享受。你仍然会嘲笑你不拥有的东西,比如第三方代码,但是你仍在测试你是通过抽象来完成手头的工作。
答案 1 :(得分:6)
看看this post。尼克提出了关于同一个例子的问题,并在评论中进行了一次非常有趣的对话。希望你觉得它有用。
答案 2 :(得分:4)
捕获$stdout
并对其进行测试,而不是尝试模拟可能输出到stdout的各种方法。毕竟,你想测试stdout而不是一些复杂的方法来模仿它。
expect { some_code }.to match_stdout( 'some string' )
使用自定义匹配器(rspec 2)
RSpec::Matchers.define :match_stdout do |check|
@capture = nil
match do |block|
begin
stdout_saved = $stdout
$stdout = StringIO.new
block.call
ensure
@capture = $stdout
$stdout = stdout_saved
end
@capture.string.match check
end
failure_message_for_should do
"expected to #{description}"
end
failure_message_for_should_not do
"expected not to #{description}"
end
description do
"match [#{check}] on stdout [#{@capture.string}]"
end
end
RSpec 3 has changed the Matcher API slightly。
failure_message_for_should
现在是failure_message
failure_message_for_should_not
现在是failure_message_when_negated
添加了supports_block_expectations?
以使块的错误更清晰。
有关完整的rspec3解决方案,请参阅Charles'答案。
答案 3 :(得分:3)
我测试它的方式是使用StringIO对象。它就像一个文件,但不会触及文件系统。为Test :: Unit语法道歉 - 随意编辑RSpec语法。
require "stringio"
output_file = StringIO.new
game = Game.new(output_file)
game.start
output_text = output_file.string
expected_text = "Welcome to Codebreaker!"
failure_message = "Doesn't include welcome message"
assert output_text.include?(expected_text), failure_message
答案 4 :(得分:1)
我看过这篇博文,帮助我解决了这个问题:
Mocking standard output in rspec。
他在块之前/之后设置,我最终在实际的rspec内部进行了这些操作,由于某种原因,我无法按照我的建议从我的spec_helper.rb开始工作。
希望它有所帮助!
答案 5 :(得分:0)
Matt对RSpec 3.0的答案的更新版本:
RSpec::Matchers.define :match_stdout do |check|
@capture = nil
match do |block|
begin
stdout_saved = $stdout
$stdout = StringIO.new
block.call
ensure
@capture = $stdout
$stdout = stdout_saved
end
@capture.string.match check
end
failure_message do
"expected to #{description}"
end
failure_message_when_negated do
"expected not to #{description}"
end
description do
"match [#{check}] on stdout [#{@capture.string}]"
end
def supports_block_expectations?
true
end
end