RSpec:我如何编写一个期望某些输出但不关心该方法的测试?

时间:2011-06-16 13:36:44

标签: ruby tdd rspec

我正试图让我的头脑围绕测试驱动的设计,特别是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上的内容,而不用看什么方法把它放在那里?

6 个答案:

答案 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