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?
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.
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.
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.
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
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!
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
精彩评论