14
votes

I'm trying to get my head around test-driven design, specifically RSpec. But I'm having trouble with some of the examples from The RSpec Book.

In the book, we test for output on $STDOUT like this:

output = double('output')
game = Game.new
output.should_receive(:puts).with('Welcome to Codebreaker!')
game.start()

Well, that works after a fashion. But why on earth should I care if the Game object uses the puts() method? If I change it to print(), should it really break the test? And, more importantly, isn't this against the one of the principals of TDD - that I should be testing what the method does (the design) rather than how it does it (the implementation)?

Is there some way I can write a test that just tests what ends up on $STDOUT, without looking at what method puts it there?

6
I'm giving the tick to David for linking to that astonishing post that asserts that mocks are evil because they couple knowledge of the internals of your code to the test. That's going to take a lot of thinking about -- but I suspect that there is a great deal of truth in it. That article makes the same suggestion as Andrew, and I think that's where I'm going on this. Thanks to everyone.Andy

6 Answers

8
votes

Create a display class with the ability to write the status out.

You production code will make use of this display object so you are free to change how you write to STDOUT. There will be just one place for this logic while your tests rely on the abstraction.

For example:

output = stub('output')
game = Game.new(output)
output.should_receive(:display).with('Welcome to Codebreaker!')
game.start()

While your production code will have something such as

class Output
  def display(message)
    # puts or whatever internally used here. You only need to change this here.
  end
end

I'd make this test pass by doing the following:

def start
  @output.display('Welcome to Codebreaker!')
end

Here the production code doesn't care how the output is displayed. It could be any form of display now the abstraction is in place.

All of the above theory is language agnostic, and works a treat. You still mock out things you don't own such as third party code, but you are still testing you are performing the job at hand via your abstraction.

6
votes

take a look at this post. Nick raised questions about the same example, and a very interesting conversation follows in the comments. Hope you find it helpful.

4
votes

Capture $stdout and test against that instead of trying to mock the various methods that might output to stdout. After all, you want to test stdout and not some convoluted method for mimicking it.

expect { some_code }.to match_stdout( 'some string' )

Which uses a custom Matcher (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 is now failure_message
failure_message_for_should_not is now failure_message_when_negated
supports_block_expectations? has been added to make errors clearer for blocks.

See Charles' answer for the complete rspec3 solution.

3
votes

The way I'd test it is with a StringIO object. It acts like a file, but doesn't touch the filesystem. Apologies for the Test::Unit syntax - feel free to edit to RSpec syntax.

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
1
votes

I came across this blog post which helped me solve this issue:

Mocking standard output in rspec.

He sets up before/after blocks, and I ended up doing them inside the actual rspec itself, for some reason I couldn't get it to work from my spec_helper.rb as recommended.

Hope it helps!

0
votes

An updated version of Matt's answer for 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