Suppress console output during RSpec tests - ruby

I am testing the class which put on the console some messages (with puts, p warnings and etc.). I am just wondering if there is any ability to suppress this output during RSpec tests ?

I suppress puts output in my classes by redirecting $stout to a text file. That way, if I need to see the output for any reason, it is there but it doesn't muddy up my test results.
#spec_helper.rb
RSpec.configure do |config|
config.before(:all, &:silence_output)
config.after(:all, &:enable_output)
end
public
# Redirects stderr and stout to /dev/null.txt
def silence_output
# Store the original stderr and stdout in order to restore them later
#original_stderr = $stderr
#original_stdout = $stdout
# Redirect stderr and stdout
$stderr = File.new(File.join(File.dirname(__FILE__), 'dev', 'null.txt'), 'w')
$stdout = File.new(File.join(File.dirname(__FILE__), 'dev', 'null.txt'), 'w')
end
# Replace stderr and stdout so anything else is output correctly
def enable_output
$stderr = #original_stderr
$stdout = #original_stdout
#original_stderr = nil
#original_stdout = nil
end
EDIT:
In response to the comment by #MyronMarston, it probably would be smarter to just insert the methods directly into before and after as blocks.
#spec_helper.rb
RSpec.configure do |config|
original_stderr = $stderr
original_stdout = $stdout
config.before(:all) do
# Redirect stderr and stdout
$stderr = File.new(File.join(File.dirname(__FILE__), 'dev', 'null.txt'), 'w')
$stdout = File.new(File.join(File.dirname(__FILE__), 'dev', 'null.txt'), 'w')
end
config.after(:all) do
$stderr = original_stderr
$stdout = original_stdout
end
end
It looks a little cleaner and keeps methods off of main.
Also, note that if you are using Ruby 2.0, you can use __dir__ instead of File.dirname(__FILE__).
EDIT2
Also it should be mentioned, that you can forward to true os /dev/null by using File::NULL as it was introduced in Ruby v 1.9.3. (jruby 1.7)
Then the code snippet will look as following:
#spec_helper.rb
RSpec.configure do |config|
original_stderr = $stderr
original_stdout = $stdout
config.before(:all) do
# Redirect stderr and stdout
$stderr = File.open(File::NULL, "w")
$stdout = File.open(File::NULL, "w")
end
config.after(:all) do
$stderr = original_stderr
$stdout = original_stdout
end
end

Try stubbing methods that make the output in a before block, e.g.
before do
IO.any_instance.stub(:puts) # globally
YourClass.any_instance.stub(:puts) # or for just one class
end
This is explicit, so you won't miss anything you don't want to miss. If you don't care about any output and the method above doesn't work you can always stub the IO object itself:
before do
$stdout.stub(:write) # and/or $stderr if needed
end

An Rspec3.0 Version would be => in spec_helper.rb
RSpec.configure do |c|
c.before { allow($stdout).to receive(:puts) }
end
it will act as before(:each)
but :each is default, so no need to write it explicitly

Tested with rspec-core (~> 3.4.0)
In describe block you could do
# spec_helper.rb
def suppress_log_output
allow(STDOUT).to receive(:puts) # this disables puts
logger = double('Logger').as_null_object
allow(Logger).to receive(:new).and_return(logger)
end
# some_class_spec.rb
RSpec.describe SomeClass do
before do
suppress_log_output
end
end
This way you have the advantage of toggling log output for specific tests. Note, this will not suppress rspec warnings, or messages from rspec.
Another way to disable warnings coming from gems:
add config.warnings = false to spec_helper
If you wanted to suppress only certain logger methods, like error, info, or warn you could do
allow_any_instance_of(Logger).to receive(:warn).and_return(nil)
To disable warnings coming from the rspec gem
allow(RSpec::Support).to receive(:warning_notifier).and_return(nil)
but this is generally discouraged because it is meant as a way to let you know you are doing something smelly in your tests.

If you want to suppress output for a single test, there is a more concise way:
it "should do something with printing" do
silence_stream(STDOUT) do
foo.print.should be_true
end
end
You may want to change STDOUT to STDERR if your test prints an error.

Updated answer for Rails 5, in a one-off situation:
before do
RSpec::Mocks.with_temporary_scope do
allow(STDOUT).to receive(:puts)
end
end
You can make this into a method in spec_helper if you'll be doing this a lot.

After trying all of these examples, I ended up using this varation which does not silence or mute binding.pry
# frozen_string_literal: true
RSpec.configure do |config|
config.before(:each) do
allow($stdout).to receive(:puts)
allow($stdout).to receive(:write)
end
end

It can be useful to inject an IO object defaulting to STDOUT. This also makes it easier to assert on the output if you want to.
E.g.
def my_method(arg, io: STDOUT)
io.puts "hello"
arg.reverse
end
And then in your test:
# Suppress it.
my_method("hi", io: StringIO.new)
# Assert on it.
io = StringIO.new
my_method("hi", io: io)
output = io.tap(&:rewind).read
expect(output).to include("hello")

You could have the object itself supress based on the environment:
class Foo
def call
puts("blah blah")
# ...
end
private
def puts(msg)
return if ENV['APP_ENV'] == 'test'
super
end
end

Related

How can I use IO#reopen with a StringIO?

I would like to capture stderr into a variable in memory, without using a file on the filesystem. (This is because if the process is kill -9'ed, the file, even if it is a Tempfile, will not be deleted.)
There is a solution for this at How do I temporarily redirect stderr in Ruby?, but its strategy is to assign a StringIO to $stderr. This will not work if the value of $stderr was copied prior to the reassignment, nor will it work if STDERR is used. As evidence:
#!/usr/bin/env ruby
stderr_sav = $stderr
$stderr = File.open(File::NULL, 'w')
$stderr.puts 'Using $stderr'
stderr_sav.puts 'Using stderr_sav'
STDERR.puts 'Using STDERR'
# Outputs:
# Using stderr_sav
# Using STDERR
In contrast, using reopen works:
#!/usr/bin/env ruby
stderr_sav = $stderr
$stderr.reopen(File.new(File::NULL, 'w'))
$stderr.puts 'Using $stderr'
stderr_sav.puts 'Using stderr_sav'
STDERR.puts 'Using STDERR'
# Outputs:
# [nothing]
Unfortunately, passing a StringIO to reopen does not work:
`reopen': no implicit conversion of StringIO into String (TypeError)
Is there any way to accomplish the capturing of $stderr without using a file?
How about a proxy which temporary delegate IO#puts to StringIO#puts
stderr_sav = $stderr
# delegate
$stdclone = $stderr.clone
$temp = StringIO.new
[:puts].each do |m|
if $temp.respond_to?(m)
$stderr.define_singleton_method "#{m}" do |*args, &block|
$temp.send(m, *args, &block)
end
end
end
# test
$stderr.puts 'secret1' # nothing
STDERR.puts 'secret2' # nothing
stderr_sav.puts 'secret3' # nothing
new_strerr = IO.new(1)
new_strerr.puts "not secret" # not secret
$stdclone.puts $temp.string # secret 1 -> 3
# reset
$stderr = $stdclone
$stderr.puts "secret4" # secret4

Suppress STDOUT during RSpec, but not Pry

I'm testing a generator, which outputs a lot of stuff to STDOUT. I want to suppress this, and there are lots of answers for that.
But I want to still be able to use pry. Right now, I have to disable the suppression if I need to pry into the test state.
I was using this code. It bypassed pry entirely:
def suppress_output(&block)
#original_stderr = $stderr
#original_stdout = $stdout
$stderr = $stdout = StringIO.new
yield(block)
$stderr = #original_stderr
$stdout = #original_stdout
#original_stderr = nil
#original_stdout = nil
end
I replaced it with this. It stops at the pry, but continues to suppress output, so you can't do anything:
def suppress_output(&block)
orig_stderr = $stderr.clone
orig_stdout = $stdout.clone
$stderr.reopen File.new("/dev/null", "w")
$stdout.reopen File.new("/dev/null", "w")
yield(block)
rescue Exception => e
$stdout.reopen orig_stdout
$stderr.reopen orig_stderr
raise e
ensure
$stdout.reopen orig_stdout
$stderr.reopen orig_stderr
end
Is there any way to have my cake and eat it too?
I'd still like an answer to this question if someone can think of a way. This isn't the only time I've had to suppress STDOUT in tests, and the scenarios haven't always been the same as this one.
However, it occurred to me today that in this case, the easier solution is to change the code, rather than the testing setup.
The generators are using Thor, which is very powerful, but has very opaque documentation past the basics and hasn't really been updated in years. When I dug around in the docs, I found there is some muting capability.
By calling add_runtime_options! in my main Cli < Thor class, I get a global --quiet option. This suppresses a lot of output, but not everything I need. #say still prints. #run itself is muted, but whatever shell commands I pass it to run are not.
Overwriting these methods takes care of the rest of my issues:
no_commands do
def quiet?
!!options[:quiet]
end
def run(command, config = {})
config[:capture] ||= quiet?
super(command, config)
end
def say(message = "", color = nil, force_new_line = (message.to_s !~ /( |\t)\Z/))
super(message, color, force_new_line) unless quiet?
end
end
I don't currently have a use-case where I would only want to suppress some things, so making it all-or-nothing works for now.
Now, I have to explicitly create the Cli instances in my tests with quiet: true, but I can run RSpec without unwanted output and still use pry.
I found this solution by Chris Hough to work in my case, adding the following configuration to spec/spec_helper:
RSpec.configure do |config|
config.before(:each) do
allow($stdout).to receive(:write)
end
end
This replaced an :all before block, which was setting the following (and reversing the assignment in an after block):
$stderr = File.open(File::NULL, "w")
$stdout = File.open(File::NULL, "w")
The fix still suppresses output while allowing Pry to function as expected.

Testing STDOUT output in Rspec

I am trying to build a spec for this statement. It is easy with 'puts'
print "'#{#file}' doesn't exist: Create Empty File (y/n)?"
RSpec 3.0+
RSpec 3.0 added a new output matcher for this purpose:
expect { my_method }.to output("my message").to_stdout
expect { my_method }.to output("my error").to_stderr
Minitest
Minitest also has something called capture_io:
out, err = capture_io do
my_method
end
assert_equals "my message", out
assert_equals "my error", err
RSpec < 3.0 (and others)
For RSpec < 3.0 and other frameworks, you can use the following helper. This will allow you to capture whatever is sent to stdout and stderr, respectively:
require 'stringio'
def capture_stdout(&blk)
old = $stdout
$stdout = fake = StringIO.new
blk.call
fake.string
ensure
$stdout = old
end
def capture_stderr(&blk)
old = $stderr
$stderr = fake = StringIO.new
blk.call
fake.string
ensure
$stderr = old
end
Now, when you have a method that should print something to the console
def my_method
# ...
print "my message"
end
you can write a spec like this:
it 'should print "my message"' do
printed = capture_stdout do
my_method # do your actual method call
end
printed.should eq("my message")
end
If your goal is only to be able to test this method, I would do it like this:
class Executable
def initialize(outstream, instream, file)
#outstream, #instream, #file = outstream, instream, file
end
def prompt_create_file
#outstream.print "'#{#file}' doesn't exist: Create Empty File (y/n)?"
end
end
# when executing for real, you would do something like
# Executable.new $stdout, $stdin, ARGV[0]
# when testing, you would do
describe 'Executable' do
before { #input = '' }
let(:instream) { StringIO.new #input }
let(:outstream) { StringIO.new }
let(:filename) { File.expand_path '../testfile', __FILE__ }
let(:executable) { Executable.new outstream, instream, filename }
specify 'prompt_create_file prompts the user to create a new file' do
executable.prompt_create_file
outstream.string.should include "Create Empty File (y/n)"
end
end
However, I want to point out that I would not test a method like this directly. Instead, I'd test the code that uses it. I was talking with a potential apprentice yesterday, and he was doing something very similar, so I sat down with him, and we reimplemented a portion of the class, you can see that here.
I also have a blog that talks about this kind of thing.

Thread-safe: Capturing the output of $stdout

I wonder how to capture the output of $stdout in a threaded environment in ruby.
A few details. I use the capture for logging purpose and I have sidekiq to process jobs in the background so the threads. Ive coded:
#previous_stdout, $stdout = $stdout, StringIO.new
self.stdout = $stdout.string
It throws me (for certain threads):
WARN: undefined method `string' for #<IO:<STDOUT>>
If you are logging things yourself, keep a StringIO log per job and write to it:
#log = StringIO.new
#log.write 'debug message'
Sidekiq also offers logging options:
https://github.com/mperham/sidekiq/wiki/Logging
Otherwise you could try something real hacky like overwriting the printing methods on Kernel and synchronizing it so you never accidentally write to the wrong $stdout. Something like:
require 'stringio'
module Kernel
##io_semaphore = Mutex.new
[ :printf, :p, :print, :puts ].each do |io_write|
hidden_io_write = "__#{io_write}__"
alias_method hidden_io_write, io_write
define_method(io_write) do |*args|
##io_semaphore.synchronize do
$stdout = Thread.current[:log] || STDOUT
self.__send__(hidden_io_write, *args)
$stdout = STDOUT
end
end
end
end
threads = 3.times.map do
Thread.new do
Thread.current[:log] = log = StringIO.new
sleep(rand)
puts "testing..."
log.string
end
end
logs = threads.map(&:value)
p logs
# => ["testing...\n", "testing...\n", "testing...\n"]

Getting the executed command from FileUtils?

When you pass the :verbose flag to a FileUtils command, the command gets printed to STDOUT. Is there a way to capture the command so it can be logged or used elsewhere?
If you look at the source for FileUtils it uses the following method for doing its verbose output:
def fu_output_message(msg) #:nodoc:
#fileutils_output ||= $stderr
#fileutils_label ||= ''
#fileutils_output.puts #fileutils_label + msg
end
i.e. it is writing the messages to #fileutils_output and by default it is using $stderr. There doesn't seem to be a method to alter #fileutils_output but you could add one:
module FileUtils
def FileUtils.fileutils_output=(new_out)
#fileutils_output = new_out
end
end
Then if you wanted to capture the commands into a file you could do:
my_fu_log = open('fu_log.log', 'w')
FileUtils.fileutils_output = my_fu_log
# FileUtils operations with :verbose => true here
my_fu_log.close
FileUtils.fileutils_output = $stderr # restore writing to stderr if you want
or if you wanted to get them in a string you could do:
log = StringIO.new
FileUtils.fileutils_output = log
# FileUtils operations with :verbose => true here
# commands are in log.string
Also, there is a module FileUtils::Verbose which basically includes FileUtils (so has all the same methods) but defaults the options to :verbose => true so if you wanted to capture lots of commands you could use this instead of specifying the option each time. (you would need to add the fileutils_output= method to this module in the same way as above.)
Alternatives
As Joshua says in the comments below, an alternative is to reassign $stderr but as he says this does mean that everything written to stderr (not just by FileUtils) is redirected. If all the FileUtils operations are happening in one go without anything else in between then this might not be an issue. So something along the lines of:
orig_stderr = $stderr # keep reference to original stderr
$stderr = my_fu_log
# use FileUtils here
$stderr = orig_stderr # restore stderr
Finally, you could reopen FileUtils and override fu_output_message(msg) itself if you need more control.
To add to Mike's answer (since I can't comment), I created this wrapper def if you want to get the output as a string:
def fileutil_out
out = StringIO.new
FileUtils.fileutils_output = out
yield
return out.string
end
mylog.info fileutil_out { FileUtils.chmod_R(0664, 'file.txt', :verbose => isVerbose) }
I ended up not using it since I wanted to revert back to #fileutils_output ||= $stderr afterwards.

Resources