RSpec cannot catch STDERR
nor STDOUT
.
Note: to_stdout and to_stderr work by temporarily replacing $stdout or $stderr, so they're not able to intercept stream output that explicitly uses STDOUT/STDERR or that uses a reference to $stdout/$stderr that was stored before the matcher is used.
STDERR
is a constant and cannot (really should not) be changed. Simplest solution is to use $stderr
instead. Better yet, use warn
to make your intentions plain and allow warnings to be redirected.
Second problem is throw
and raise
are not equivalent. throw
is for control flow, raise
is for exceptions. throw
is like Ruby's goto
. Use raise
instead.
class MyClass
# You don't need an empty initialize, it is inherited from Object.
def will_raise
print_message_to_stderr
raise MyError
end
def print_message_to_stderr
warn 'Boom!'
end
end
class MyError < RuntimeError
end
RSpec.describe MyClass do
it 'outputs to stderr and raises MyError' do
instance = described_class.new
expect {
instance.will_raise
}.to output("Boom!\n").to_stderr
.and raise_error(MyError)
end
end
Since your "warning" immediately precedes an exception, it's not really a warning, it's an error message. If you make it part of the Exception it will make handling errors much simpler. Do this by providing a default message. If the message is complex, you can override #message
.
Now the error and message can be caught and controlled as one unit. The code and tests are much simpler.
class MyClass
def will_raise
raise MyError
end
end
class MyError < RuntimeError
def initialize(message = "Boom!")
super
end
end
RSpec.describe MyClass do
it 'raises MyError' do
instance = described_class.new
expect {
instance.will_raise
}.to raise_error(MyError, "Boom!")
end
end
True warnings and informational messages are better done via Logger
. Logged messages are easier to control and redirect.
require 'logger'
class MyClass
def logger
@logger ||= Logger.new(STDERR)
end
def will_raise
logger.warn("Lookout!")
raise MyError
end
end
class MyError < RuntimeError
def initialize(message = "Boom!")
super
end
end
One generally does not test log messages, they're usually turned off during testing, but if you really wanted to you can check using a message expectation and with
to specify its arguments.
RSpec.describe MyClass do
it 'raises MyError and logs a warning' do
instance = described_class.new
expect(instance.logger).to receive(:warn)
.with("Lookout!")
expect {
instance.will_raise
}.to raise_error(MyError, "Boom!")
end
end
Generally you don't want to be trapping output if you can avoid it. You want to check method calls instead.