Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

suppresing output to console with ruby

I am writing some unit tests like the following:

def executing_a_signal
  a_method(a_signal.new, a_model, a_helper);
  assert_equal(new_state, a_model.state)
end

The tests work fine, but the method which runs just before the assertion to execute the logic prints various messages to the console, mainly via puts.

Is there a quick, perhaps built-in, way to suppress that output to the console? I am only interested in the final effect of the method on the model object, and for the sake of keeping the console clean basically, I was hoping to find a way to simply prevent all output to the console without re-writing or commenting out those puts statements just for my tests.

It is definitely not a critical issue, but would very much like to hear any thoughts or ideas (or workaround) on it.

like image 206
denchr Avatar asked Sep 30 '09 03:09

denchr


3 Answers

I use the following snippet in tests to capture and test STDOUT

def capture_stdout(&block)
  original_stdout = $stdout
  $stdout = fake = StringIO.new
  begin
    yield
  ensure
    $stdout = original_stdout
  end
  fake.string
end

With this method, the above would become:

def executing_a_signal
  capture_stdout { a_method(a_signal.new, a_model, a_helper) }
  assert_equal(new_state, a_model.state)
end
like image 166
cldwalker Avatar answered Oct 21 '22 17:10

cldwalker


A slightly cleaner take on @cldwalker's solution:

def silenced
  $stdout = StringIO.new

  yield
ensure
  $stdout = STDOUT
end

silenced do
  something_that_prints
end
like image 10
ndnenkov Avatar answered Oct 21 '22 17:10

ndnenkov


There's two solutions: redirecting where puts writes to (the solution given by @cldwalker above), or overwriting the puts method itself to be a no-op. (The implementation should be obvious: module Kernel; def puts(*args) end end).

However, in this case, what would really be the best solution is "listening to your tests". Because, oftentimes when something is awkward to test, your tests are really trying to tell you that something is wrong with your design. In this case, I smell a violation of the Single Responsibility Principle: why the heck does a Model object need to know how to write to the console? Its responsibility is representing a Domain Concept, not logging! That's what Logger objects are for!

So, an alternative solution would be to have the Model object delegate the responsibility for logging to a Logger object, and use dependency injection to inject the Model object with a suitable Logger object. That way, you can simply inject a fake logger for the test. Here's an example:

#!/usr/bin/env ruby

class SomeModel
  def initialize(logger=Kernel) @logger = logger end
  def some_method_that_logs; @logger.puts 'bla' end
end

require 'test/unit'
require 'stringio'
class TestQuietLogging < Test::Unit::TestCase
  def setup; @old_stdout, $> = $>, (@fake_logdest = StringIO.new) end
  def teardown; $> = @old_stdout end

  def test_that_default_logging_is_still_noisy
    SomeModel.new.some_method_that_logs

    assert_equal "bla\n", @fake_logdest.string
  end

  def test_that_logging_can_be_made_quiet
    fake_logger = Object.new
    def fake_logger.puts *args; end

    SomeModel.new(fake_logger).some_method_that_logs

    assert_equal '', @fake_logdest.string
  end
end

At the very least, the Model object should take the IO object that it is logging to as an argument, so that you can simply inject StringIO.new into it for the test:

#!/usr/bin/env ruby

class SomeModel
  def initialize(logdest=$>) @logdest = logdest end
  def some_method_that_logs; @logdest.puts 'bla' end
end

require 'test/unit'
require 'stringio'
class TestQuietLogging < Test::Unit::TestCase
  def setup; @old_stdout, $> = $>, (@fake_logdest = StringIO.new) end
  def teardown; $> = @old_stdout end

  def test_that_default_logging_is_still_noisy
    SomeModel.new.some_method_that_logs

    assert_equal "bla\n", @fake_logdest.string
  end

  def test_that_logging_can_be_made_quiet
    fake_logdest = (@fake_logdest = StringIO.new)

    SomeModel.new(fake_logdest).some_method_that_logs

    assert_equal '', @fake_logdest.string
    assert_equal "bla\n", fake_logdest.string
  end
end

If you still want to be able to just say puts whatever in your Model or you are afraid that someone might forget to call puts on the logger object, you can provide your own (private) puts method:

class SomeModel
  def initialize(logdest=$>) @logdest = logdest end
  def some_method_that_logs; puts 'bla' end
  private
  def puts(*args) @logdest.puts *args end
end
like image 4
Jörg W Mittag Avatar answered Oct 21 '22 18:10

Jörg W Mittag