Rspec should_receive and abort - ruby

I have following code:
class Init
def initialize(global_options, options, args)
abort "Key file must be given!" if (key_file = args.first).nil?
begin
#secret = File.read(key_file)
rescue
abort "Cannot read key file #{key_file}"
end
stdout, stderr, status = Open3.capture3("git status -uno --porcelain")
#...
and following specs for it:
describe Rgc::Init do
context :initialize do
it 'should abort when no key file given' do
Rgc::Init.any_instance.should_receive(:abort)
.with("Key file must be given!")
Rgc::Init.new({}, {}, [])
end
end
end
And I get following output:
Failure/Error: Rgc::Init.new({}, {}, [])
#<Rgc::Init:0x0000000157f728> received :abort with unexpected arguments
expected: ("Key file must be given!")
got: ("Cannot read key file ")
should_receive method somehow block abort to take a place. How to fix the spec to check that app has been aborted and with specific message?

Your two expectations need to be treated as separate things. First, as you noticed, abort is now stubbed and therefore doesn't actually abort execution of your code -- it's really just acting like a puts statement now. Because of this, abort is being called twice: once with your expected message, and then again within your begin block. And if you add { abort } to the end of your expectation, it will actually abort, but that will also abort your test suite.
What you should do is use a lambda and make sure the abort is called:
lambda { Rgc::Init.new({}, {}, []) }.should raise_error SystemExit
abort prints the message you give it to stderr. To capture that, you can add a helper to temporarily replace stderr with a StringIO object, which you can then check the contents of:
def capture_stderr(&block)
original_stderr = $stderr
$stderr = fake = StringIO.new
begin
yield
ensure
$stderr = original_stderr
end
fake.string
end
it 'should abort when no key file given' do
stderr = capture_stderr do
lambda { Rgc::Init.new({}, {}, []) }.should raise_error SystemExit
end
stderr.should == "Key file must be given!\n"
end
(Credit to https://stackoverflow.com/a/11349621/424300 for the stderr replacement)

Related

How can I exit a ruby program on strg-c if a SystemExit exception is being catched

The code which I can not interrupt by using strg-c (Ctrl-C) :
orig_std_out = STDOUT.clone
orig_std_err = STDERR.clone
STDOUT.reopen('/dev/null', 'w')
STDERR.reopen('/dev/null', 'w')
name = cookbook_name(File.join(path, 'Metadata.rb'))
error = 0
begin
::Chef::Knife.run(['cookbook', 'site', 'show', "#{name}"])
rescue SystemExit
error = 1
end
.
.
.
In my understanding this behaviour would be reasonable if I would rescue Exception, but in this case I am basically catching siblings which only share their parent exception Exception.
I have already tried to rescue the exception Interrupt and SignalException explicitly.
EDIT1: In the hope of clarifying my question I added the following code which i tried:
begin
::Chef::Knife.run(['cookbook', 'site', 'show', "#{name}"])
rescue SystemExit => e
msg1 = e.message
error = 1
rescue Interrupt
msg2 = "interrupted"
end
In both cases - SystemExit thrown by Knife.run and thrown by Ctrl-C - e.message returns "exit". This does not only mean, that Ctrl-C throws a SystemExit whereas I am expecting it to throw an Interrupt, but also that the error message is the same.
I guess that I have got a major misunderstanding in how ruby works there, since I am not very familiar with ruby.
EDIT2: Further testing revealed that some Ctrl-C interrupts are rescued by rescue Interrupt. Is it possible that the command ::Chef::Knife.run(['cookbook', 'site', 'show', "#{name}"]), which takes about 3-5 seconds to run, creates some kind of subprocess which responds to a Ctrl-C, but always closes with a SystemExit and that rescue Interruptonly works when it is interrupted just the moment this subprocess is not running? If this is the case, how am I going to be able to Interrupt the whole program?
EDIT3: I initially wanted to attach all the methods which get called on calling Knife.run, however, this would have been too many LoC, although I think my guess that a subcommand is executed was right. The chef gem code can be found here. Thus, the following excerpt is only the part which is the problematic one in my opinion:
rescue Exception => e
raise if raise_exception || Chef::Config[:verbosity] == 2
humanize_exception(e)
exit 100
end
Which leads to the question: How can I catch a Ctrl-C which is already rescued by a subcommand?
I have done gem install chef. Now I try another solution, replacing only run_with_pretty_exceptions, but don't know which require to put in the script. I did this :
require 'chef'
$:.unshift('Users/b/.rvm/gems/ruby-2.3.3/gems/chef-13-6-4/lib')
require 'chef/knife'
But then :
$ ruby chef_knife.rb
WARNING: No knife configuration file found
ERROR: Error connecting to https://supermarket.chef.io/api/v1/cookbooks/xyz, retry 1/5
...
So, without the whole infrastructure, I can't test the following solution. The idea is that in Ruby you can reopen an existing class and replace a method defined elsewhere. I have to leave you check it :
# necessary require of chef and knife ...
class Chef::Knife # reopen the Knife class and replace this method
def run_with_pretty_exceptions(raise_exception = false)
unless respond_to?(:run)
ui.error "You need to add a #run method to your knife command before you can use it"
end
enforce_path_sanity
maybe_setup_fips
Chef::LocalMode.with_server_connectivity do
run
end
rescue Exception => e
raise if e.class == Interrupt # <---------- added ********************
raise if raise_exception || Chef::Config[:verbosity] == 2
humanize_exception(e)
exit 100
end
end
name = cookbook_name(File.join(path, 'Metadata.rb'))
error = 0
begin
::Chef::Knife.run(['cookbook', 'site', 'show', "#{name}"])
rescue SystemExit => e
puts "in rescue SystemExit e=#{e.inspect}"
error = 1
rescue Interrupt
puts 'in rescue Interrupt'
end
raise if e.class == Interrupt will re-raise Interrupt if it is one.
Normally I run ruby -w to display diagnostics, which would be like this :
$ ruby -w ck.rb
ck.rb:9: warning: method redefined; discarding old run_with_pretty_exceptions
ck.rb:4: warning: previous definition of run_with_pretty_exceptions was here
Unfortunately there are so many uninitialized variables and circular require warnings in this gem that this option produces un unmanageable output.
The drawback of this solution is that you have to keep a documentation track of this change, and in case of Chef's release change, somebody has to verify if the code of run_with_pretty_exceptions has changed.
Please give me a feedback.
===== UPDATE =====
There is a less intrusive solution, which consists in defining an exit method in Chef::Knife.
When you see exit 100, i.e. a message without receiver, the implicit receiver is self, it is equivalent to self.exit 100. In our case, self is the object created by instance = subcommand_class.new(args), and which is the receiver in instance.run_with_pretty_exceptions.
When a message is sent to an object, the message search mechanism starts looking in the class of this object. If there is no method with this name in the class, the search mechanism looks in included modules, then the superclass, etc until it reaches Object, the default superclass of Chef::Knife. Here it finds Object#exit and executes it.
After defining an exit method in Chef::Knife, the message search mechanism, when it encounters exit 100 with an instance of Chef::Knife as implicit receiver, will first find this local method and execute it. By previously aliasing the original Object#exit, it is still possible to call the original Ruby method which initiates the termination of the Ruby script. This way the local exit method can decide either to call the original Object#exit or take other action.
Following is a complete example which demonstrates how it works.
# ***** Emulation of the gem *****
class Chef end
class Chef::Knife
def self.run(x)
puts 'in original run'
self.new.run_with_pretty_exceptions
end
def run_with_pretty_exceptions
print 'Press Ctrl_C > '
gets
rescue Exception => e
puts
puts "in run_with_pretty...'s Exception e=#{e.inspect} #{e.class}"
raise if false # if raise_exception || Chef::Config[:verbosity] == 2
# humanize_exception(e)
puts "now $!=#{$!.inspect}"
puts "about to exit, self=#{self}"
exit 100
end
end
# ***** End of gem emulation *****
#----------------------------------------------------------------------
# ***** This is what you put into your script. *****
class Chef::Knife # reopen the Knife class and define one's own exit
alias_method :object_exit, :exit
def exit(p)
puts "in my own exit with parameter #{p}, self=#{self}"
puts "$!=#{$!.inspect}"
if Interrupt === $!
puts 'then about to raise Interrupt'
raise # re-raise Interrupt
else
puts 'else about to call Object#exit'
object_exit(p)
end
end
end
begin
::Chef::Knife.run([])
rescue SystemExit => e
puts "in script's rescue SystemExit e=#{e.inspect}"
rescue Interrupt
puts "in script's rescue Interrupt"
end
Execution. First test with Ctrl-C :
$ ruby -w simul_chef.rb
in original run
Press Ctrl_C > ^C
in run_with_pretty...'s Exception e=Interrupt Interrupt
now $!=Interrupt
about to exit, self=#<Chef::Knife:0x007fb2361c7038>
in my own exit with parameter 100, self=#<Chef::Knife:0x007fb2361c7038>
$!=Interrupt
then about to raise Interrupt
in script's rescue Interrupt
Second test with a hard interrupt.
In one terminal window :
$ ruby -w simul_chef.rb
in original run
Press Ctrl_C >
In another terminal window :
$ ps -ef
UID PID PPID C STIME TTY TIME CMD
0 1 0 0 Fri01PM ?? 0:52.65 /sbin/launchd
...
0 363 282 0 Fri01PM ttys000 0:00.02 login -pfl b /bin/bash -c exec -la bash /bin/bash
501 364 363 0 Fri01PM ttys000 0:00.95 -bash
501 3175 364 0 9:51PM ttys000 0:00.06 ruby -w simul_chef.rb
...
$ kill 3175
Back in the first terminal :
in run_with_pretty...'s Exception e=#<SignalException: SIGTERM> SignalException
now $!=#<SignalException: SIGTERM>
about to exit, self=#<Chef::Knife:0x007fc5a79d70a0>
in my own exit with parameter 100, self=#<Chef::Knife:0x007fc5a79d70a0>
$!=#<SignalException: SIGTERM>
else about to call Object#exit
in script's rescue SystemExit e=#<SystemExit: exit>
Considering the code you originally posted, all you have to do is inserting at the beginning, but after the necessary require :
class Chef::Knife # reopen the Knife class and define one's own exit
alias_method :object_exit, :exit
def exit(p)
if Interrupt === $!
raise # re-raise Interrupt
else
object_exit(p)
end
end
end
So there is no need to touch the original gem.
The following code shows how I am able to interrupt after all:
interrupted = false
trap("INT") { interrupted = true} #sent INT to force exit in Knife.run and then exit
begin
::Chef::Knife.run(['cookbook', 'site', 'show', "#{name}"]) #exits on error and on interrupt with 100
if interrupted
exit
end
rescue SystemExit => e
if interrupted
exit
end
error = 1
end
The drawback still is, that I am not exactly able to interrupt the Knife.run, but only able to trap the interrupt and check after that command whether an interrupt was triggered. I found no way to trap the interrupt and "reraise" it at the same time, so that I am at least able to force an exit out of Knife.run which I can then exit manually.

How do I get ruby to print a full backtrace that includes arguments passed to functions?

Sometimes backtrace is enough to diagnose problem. But sometimes reason of crash is not obvious without knowledge what was passed to function.
Getting information what was passed to function that caused crash would be quite useful, especially in cases where reproducing is not obvious because it was caused by for example exception in network connection, weird user input or because program is depends on randomisation or processes data from external sensor.
Lets say that there is following program
def handle_changed_input(changed_input)
raise 'ops' if changed_input =~ /magic/
end
def do_something_with_user_input(input)
input = "#{input.strip}c"
handle_changed_input(input)
end
input = gets
do_something_with_user_input(input)
where user typed "magic" as input. Normally one has
test.rb:2:in `handle_changed_input': ops (RuntimeError)
from test.rb:7:in `do_something_with_user_input'
from test.rb:11:in `<main>'
as output. What one may do to show also what was passed to function? Something like
test.rb:2:in `handle_changed_input("magic")': ops (RuntimeError)
from test.rb:7:in `do_something_with_user_input("magi\n")'
from test.rb:11:in `<main>'
It would be useful in many situations (and not truly useful where parameters are not representable as strings of reasonable legth, there is a good reason why it is not enabled by default).
How one may add this functionality? It is necessary that program works as usually during normal operation and preferably there is no additional output before crash.
I tried for example
def do_something_with_user_input(input)
method(__method__).parameters.map do |_, name|
puts "#{name}=#{binding.local_variable_get(name)}"
end
raise 'ops' if input =~ /magic/
end
input = gets
found in Is there a way to access method arguments in Ruby? but it would print on every single entrance to function what both would flood output and make program significantly slower.
I don't have a complete solution but... But you can get method arguments of all called methods in controlled environment with TracePoint class from Ruby core lib.
Look at the example:
trace = TracePoint.new(:call) do |tp|
puts "===================== #{tp.method_id}"
b_self = tp.binding.eval('self')
names = b_self.method(tp.method_id).parameters.map(&:last)
values = names.map { |name| tp.binding.eval(name.to_s) }
p names.zip(values)
end
trace.enable
def method_a(p1, p2, p3)
end
method_a(1, "foobar", false)
#=> ===================== method_a
#=> [[:p1, 1], [:p2, "foobar"], [:p3, false]]
To print exception backtraces, Ruby uses the C function exc_backtrace from error.c (exc_backtrace on github). Unless you patch Ruby with the functionality you need, I don't think there a way to change exception backtrace outputs.
Here is a snippet (trace.rb) you might find useful:
set_trace_func -> (event, file, line, id, binding, classname) do
if event == 'call' && meth = binding.eval('__method__')
params = binding.method(meth).parameters.select{|e| e[0] != :block}
values = params.map{|_, var| [var, binding.local_variable_get(var)]}
printf "%8s %s:%-2d %15s %8s %s\n", event, file, line, id, classname, values.inspect
else
printf "%8s %s:%-2d %15s %8s\n", event, file, line, id, classname
end
end
def foo(a,b = 0)
bar(a, foo: true)
end
def bar(c, d = {})
puts "!!!buz!!!\n"
end
foo('lol')
The output of that snippet is:
c-return /path/to/trace.rb:1 set_trace_func Kernel
line /path/to/trace.rb:12
c-call /path/to/trace.rb:12 method_added Module
c-return /path/to/trace.rb:12 method_added Module
line /path/to/trace.rb:16
c-call /path/to/trace.rb:16 method_added Module
c-return /path/to/trace.rb:16 method_added Module
line /path/to/trace.rb:20
call /path/to/trace.rb:12 foo Object [[:a, "lol"], [:b, 0]]
line /path/to/trace.rb:13 foo Object
call /path/to/trace.rb:16 bar Object [[:c, "lol"], [:d, {:foo=>true}]]
line /path/to/trace.rb:17 bar Object
c-call /path/to/trace.rb:17 puts Kernel
c-call /path/to/trace.rb:17 puts IO
c-call /path/to/trace.rb:17 write IO
!!!buz!!!
c-return /path/to/trace.rb:17 write IO
c-return /path/to/trace.rb:17 puts IO
c-return /path/to/trace.rb:17 puts Kernel
return /path/to/trace.rb:18 bar Object
return /path/to/trace.rb:14 foo Object
I hope that helps you as much as it helped me.
I think that it is possible. The code below is not perfect and would require some additional work, but it caputers the primary idea of a stacktrace with argument values. Please note, that in order to know the call site, I am zipping the original stacktrace with the entry sites catched by trace function. To distinguishe these entries I use '>' and '<' respectively.
class Reporting
def self.info(arg1)
puts "*** #{arg1} ***"
end
end
def read_byte(arg1)
Reporting.info(arg1)
raise Exception.new("File not found")
end
def read_input(arg1)
read_byte(arg1)
end
def main(arg1)
read_input(arg1)
end
class BetterStacktrace
def self.enable
set_trace_func -> (event, file, line, id, binding, classname) do
case event
when 'call'
receiver_type = binding.eval('self.class')
if receiver_type == Object
meth = binding.eval('__method__')
params = binding.method(meth).parameters.select{|e| e[0] != :block}
values = params.map{|_, var| [var, binding.local_variable_get(var)]}
self.push(event, file, line, id, classname, values)
else
self.push(event, file, line, id, classname)
end
when 'return'
self.pop
when 'raise'
self.push(event, file, line, id, classname)
Thread.current[:_keep_stacktrace] = true
end
end
end
def self.push(event, file, line, id, classname, values=nil)
Thread.current[:_saved_stacktrace] = [] unless Thread.current.key?(:_saved_stacktrace)
unless Thread.current[:_keep_stacktrace]
if values
values_msg = values.map(&:last).join(", ")
msg = "%s:%d:in `%s(%s)'" % [file, line, id, values_msg]
else
msg = "%s:%d:in `%s'" % [file, line, id]
end
Thread.current[:_saved_stacktrace] << msg
end
end
def self.pop()
Thread.current[:_saved_stacktrace] = [] unless Thread.current.key?(:_saved_stacktrace)
unless Thread.current[:_keep_stacktrace]
value = Thread.current[:_saved_stacktrace].pop
end
end
def self.disable
set_trace_func nil
end
def self.print_stacktrace(calls)
enters = Thread.current[:_saved_stacktrace].reverse
calls.zip(enters).each do |call, enter|
STDERR.puts "> #{enter}"
STDERR.puts "< #{call}"
end
Thread.current[:_saved_stacktrace] = []
end
end
BetterStacktrace.enable
begin
main(10)
rescue Exception => ex
puts "--- Catched ---"
puts ex
BetterStacktrace.print_stacktrace(ex.backtrace)
end
BetterStacktrace.disable
begin
main(10)
rescue Exception
puts "--- Catched ---"
puts ex
puts ex.backtrace
end
The output of the above code is as follows:
*** 10 ***
--- Catched ---
File not found
> work/tracing_with_params.rb:10:in `read_byte'
< work/tracing_with_params.rb:10:in `read_byte'
> work/tracing_with_params.rb:8:in `read_byte(10)'
< work/tracing_with_params.rb:14:in `read_input'
> work/tracing_with_params.rb:13:in `read_input(10)'
< work/tracing_with_params.rb:18:in `main'
> work/tracing_with_params.rb:17:in `main(10)'
< work/tracing_with_params.rb:82:in `<main>'
*** 10 ***
--- Catched ---
File not found
work/tracing_with_params.rb:10:in `read_byte'
work/tracing_with_params.rb:14:in `read_input'
work/tracing_with_params.rb:18:in `main'
work/tracing_with_params.rb:82:in `<main>'
EDIT:
The calls to class functions are not recorded. This has to be fixed in order for the stacktrace printing function not to get invalid output.
Moreover I used the STDERR as output to easily get one or the other output. You can change it if you wish.
MAX_STACK_SIZE = 200
tracer = proc do |event|
if event == 'call' && caller_locations.length > MAX_STACK_SIZE
fail "Probable Stack Overflow"
end
end
set_trace_func(tracer)

Detecting and handling NonLocalExit from ruby

I have this code that can trigger in production the "Non local exit detected!" branch. I can't understand how that can happen, since even a return will trigger a NonLocalExit exception. Even a throw will trigger an exception.
Is there any way to have exception_raised and yield_returned both false?
def transaction
yield_returned = exception_raised = nil
begin
if block_given?
result = yield
yield_returned = true
puts 'yield returned!'
result
end
rescue Exception => exc
exception_raised = exc
ensure
if block_given?
unless yield_returned or exception_raised
puts 'Non local exit detected!'
end
end
end
end
transaction do
puts 'good block!'
end
transaction do
puts 'starting transaction with block with return'
return
puts 'this will not show'
end
Output:
good block!
yield returned!
starting transaction with block with return
I want to somehow output 'Non local exit detected!'. I know this happens in production, but I can't make it happen in development. Tried it with a return and with a throw, but they both raise an exception. Any other way?
The problem was with my reproduction, and returning from the top-most level in Ruby. You can return from a method, where there's a stack, but if you return from Kernel, you'll get a LocalJumpError.
Assuming the previous method transaction(). In matters in what context you call return:
def lol
transaction do
puts 'Block in method: starting transaction with block with return'
return
puts 'this will not show'
end
end
lol()
transaction do
puts 'block in Kernel: starting transaction with block with return'
return
puts 'this will not show'
end
Output:
$ ruby local_jump_error.rb
# Running from method:
Block in method: starting transaction with block with return
yield_returned=nil, exception_raised=nil
Non local exit detected!
# Running without method:
block in Kernel: starting transaction with block with return
yield_returned=nil, exception_raised=#<LocalJumpError: unexpected return>
local_jump_error.rb:45: unexpected return (LocalJumpError)
from local_jump_error.rb:6:in `transaction'
from local_jump_error.rb:43
I don't know your level with Ruby, but to exit from a block you must use break instead of return. In this case, break also accepts a value in the same way that return does, meaning that a break will conceptually assign the variable result with the value from break.
In case of a throw, it will raise an exception, so it will always unwind the call stack until if finds a rescue statement, thus it will run the code exception_raised = exc.
You can fine tune the rescue to use LocalJumpError instead of Exception to only catch blocks that have return in them. All other exception types will then not stop at that rescue.
I wonder if you meant to write:
if block_given?
if yield_returned.nil? && exception_raised
puts 'Non local exit detected!'
end
end
If you make this change, the code will produce the 'Non local exit detected!', for the second method call of #transaction with the return.
When you wrote unless yield_returned or expection_raised, the if clause would get evaluated only when both the variables would have been false. And as I understand that is not possible.
And as a side note, as was suggested in the other answer, one should not rescue Exception, LocalJumpError should be enough.

Collecting exceptions in ruby script

I'm writing a script which collects data from various url's. I want to collect errors from begin rescue blocks into an array to output them when the program runs in verbose mode. With normal use, a failed connection is ignored and the script moves on to the next url.
I thought the best way to do this would be to create an array errArray = Array.new at the top of the script to hold errors, and then do:
rescue Exception => e
errArray << e.message
in various functions to log errors. The die function outputs the array using p unless it is empty. However, I get the error
Undefined local variable or method 'errArray'
Any help (and constructive criticism) appreciated.
EDIT: die function:
def die(e)
p errorArray unless errorArray.empty?
# Some other irrelevant code
end
errArray is not global variable and therefore methods have no access to it. You can declare it as a global variable by $err_array.
However the best solution would be create a simple class:
class ExceptionCollector
def collect
yield
rescue => e
errors << e.message
end
def errors
#errors ||= []
end
end
And then simple:
$logger = ExceptionCollector.new
$logger.collect do
# this may raise an exception
end
def foo
$logger.collect do
# another exception
end
end
$logger.errors #=> list of errors

How to spec methods that exit or abort

I have a method being triggered from a CLI that has some logical paths which explicitly exit or abort. I have found that when writing specs for this method, RSpec marks it as failing because the exits are exceptions. Here's a a bare bones example:
def cli_method
if condition
puts "Everything's okay!"
else
puts "GTFO!"
exit
end
end
I can wrap the spec in a lambda with should raise_error(SystemExit), but that disregards any assertions that happen inside the block. To be clear: I'm not testing the exit itself, but the logic that happens before it. How might I go about speccing this type of method?
Simply put your assertions outside of the lambda, for example:
class Foo
attr_accessor :result
def logic_and_exit
#result = :bad_logic
exit
end
end
describe 'Foo#logic_and_exit' do
before(:each) do
#foo = Foo.new
end
it "should set #foo" do
lambda { #foo.logic_and_exit; exit }.should raise_error SystemExit
#foo.result.should == :logics
end
end
When I run rspec, it correctly tells me:
expected: :logics
got: :bad_logic (using ==)
Is there any case where this wouldn't work for you?
EDIT: I added an 'exit' call inside the lambda to hande the case where logic_and_exit doesn't exit.
EDIT2: Even better, just do this in your test:
begin
#foo.logic_and_exit
rescue SystemExit
end
#foo.result.should == :logics
New answer to cover Rspec 3's expect syntax.
Testing the output
Just to test what you actually want (ie. you're not testing the exception, or a value response,) what has been output to STDOUT.
When condition is false
it "has a false condition" do
# NOTE: Set up your condition's parameters to make it false
expect {
begin cli_method
rescue SystemExit
end
}.to output("GTFO").to_stdout # or .to_stderr
end
When condition is true
it "has a true condition" do
# NOTE: Set up your condition's parameters to make it true
expect {
begin cli_method
rescue SystemExit
end
}.to output("Everything's okay!").to_stdout
end
Note that output("String").to_... can accept a Regex eg.
output(/^Everything's okay!$/).to_stdout
It can also capture from stderr eg.
output("GTFO").to_stderr
(Which would be a better place to send it, for the OP's example.)
Testing the Exit
You can separately test that the false condition also raises SystemExit
it "exits when condition is false" do
# NOTE: Set up your condition's parameters to make it false
expect{cli_method}.to raise_error SystemExit
end
it "doesn't exit when condition is true" do
# NOTE: Set up your condition's parameters to make it true
expect{cli_method}.not_to raise_error SystemExit
end
I can wrap the spec in a lambda with should raise_error(SystemExit),
but that disregards any assertions that happen inside the block.
I don't see a difference putting tests inside or outside the lambda. In either case, the failure message is a bit cryptic:
def cli_method(condition)
if condition
puts "OK"
else
puts "GTFO"
exit
end
end
describe "cli_method" do
context "outside lambda" do
# passing
it "writes to STDOUT when condition is false" do
STDOUT.should_receive(:puts).with("GTFO")
lambda {
cli_method(false)
}.should raise_error(SystemExit)
end
# failing
it "does not write to STDOUT when condition is false" do
STDOUT.should_not_receive(:puts).with("GTFO")
lambda {
cli_method(false)
}.should raise_error(SystemExit)
end
end
context "inside lambda" do
# passing
it "writes to STDOUT when condition is false" do
lambda {
STDOUT.should_receive(:puts).with("GTFO")
cli_method(false)
}.should raise_error(SystemExit)
end
# failing
it "does not write to STDOUT when condition is false" do
lambda {
STDOUT.should_not_receive(:puts).with("GTFO")
cli_method(false)
}.should raise_error(SystemExit)
end
end
end
# output
.F.F
Failures:
1) cli_method outside lambda does not write to STDOUT when condition is false
Failure/Error: lambda {
expected SystemExit, got #<RSpec::Mocks::MockExpectationError: (#<IO:0xb28cd8>).puts("GTFO")
expected: 0 times
received: 1 time>
# ./gtfo_spec.rb:23:in `block (3 levels) in <top (required)>'
2) cli_method inside lambda does not write to STDOUT when condition is false
Failure/Error: lambda {
expected SystemExit, got #<RSpec::Mocks::MockExpectationError: (#<IO:0xb28cd8>).puts("GTFO")
expected: 0 times
received: 1 time>
# ./gtfo_spec.rb:39:in `block (3 levels) in <top (required)>'

Resources