codegourmet

savory code and other culinary highlights

Ruby: Capturing and Mocking Stdout

| Comments

Sometimes, you want to assert that a method under test outputs some specific information to stdout. Or you want to silence all stdout from a library gone rogue (this happened to me once when I had to include a third-party library into a ruby C extension gem).

I wrote a Gem that does this, and MiniTest and RSpec already have matchers for this:

OStreamCatcher
RSpec output matchers
MiniTest output assertions

The drawback with the MiniTest matcher is that it doesn’t suppress output, so when testing a very talkative module, your test results might look pretty ugly.

Capturing output - first version

You might want to catch/mute output selectively not only via test matchers, but also during runtime. Here’s how to do this:

1
2
3
4
5
6
7
8
9
10
stdout_orig = $stdout
$stdout = stdout_mock = StringIO.new

begin
  print "Hello World!"
ensure
  $stdout = stdout_orig
end

stdout.string # "Hello World!"

A little explanation:

The $stdout Ruby variable represents the console output stream. It’s compatible to StringIO. Any StringIO object we assign to it is used by ruby’s output methods.

StringIO#string() returns the contents of this object, in our case anything that has been written to it via puts() or print().

In the code above, we switch stdout to our own StringIO object, call the code whose output we’re interested in and then restore the original stdout. Note that we do the restoring in an ensure block so that we can be sure $stdout is “repaired” even if we’re encountering an exception!

That’s a lot of clutter just for catching a string, so let’s put this into a method:

Capturing output - helper method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def capture_stdout(&block)
  stdout_orig = $stdout
  stdout_mock = StringIO.new
  $stdout = stdout_mock

  begin
    result = block.call
  ensure
    $stdout = stdout_orig
  end

  [result, stdout_mock.string]
end


result, output = capture_stdout do
  puts "Hello World!"
  42
end

result # => 42
output # => "Hello World!"

This helper returns the result of the passed block and everything that has been written to stdout during execution of said block. Since a new stdout replacement is created every time the helper is called, we can be sure that we’re only dealing with the passed block’s output.

Capturing output - Gem

The OStreamCatcher Gem already implements above method:

1
2
3
4
5
6
7
8
9
require 'o_stream_catcher'

result, stdout, stderr = OStreamCatcher.catch do
  print "Hello World!"
  42
end

result # => 42
stdout # => "Hello World!"

Beware of STDOUT

You remember Coworker Greg, who made an art out of breaking the logfile analyzer, don’t you?

1
puts "~=* ERROR: I N V A L I D ;) *=~"

Well, let’s put a stop to that!

1
2
3
4
# OH C'MON GREG, NOT AGAIN!
def test_uses_logger do
  assert_empty(capture_stdout { get 'index' })
end

That’ll teach him, right? Bad news: good old Greg has some tricks of his own up his sleeve!

1
2
3
4
def index
  $stdout = STDOUT
  puts "ehehe..."
end

STDOUT is the global “original” stdout and can’t be overridden. So there’s still some potential for output escaping our stdout override, albeit restricted to the odd corner case.

Happy Coding! – codegourmet

Comments