How can I validate exits and aborts in RSpec?
RubyRspecMockingRuby Problem Overview
I am trying to spec behaviors for command line arguments my script receives to ensure that all validation passes. Some of my command line arguments will result in abort
or exit
being invoked because the parameters supplied are missing or incorrect.
I am trying something like this which isn't working:
# something_spec.rb
require 'something'
describe Something do
before do
Kernel.stub!(:exit)
end
it "should exit cleanly when -h is used" do
s = Something.new
Kernel.should_receive(:exit)
s.process_arguments(["-h"])
end
end
The exit
method is firing cleanly preventing RSpec from validating the test (I get "SystemExit: exit").
I have also tried to mock(Kernel)
but that too is not working as I'd like (I don't see any discernible difference, but that's likely because I'm not sure how exactly to mock Kernel and make sure the mocked Kernel is used in my Something
class).
Ruby Solutions
Solution 1 - Ruby
try this:
module MyGem
describe "CLI" do
context "execute" do
it "should exit cleanly when -h is used" do
argv=["-h"]
out = StringIO.new
lambda { ::MyGem::CLI.execute( out, argv) }.should raise_error SystemExit
end
end
end
end
Solution 2 - Ruby
Using the new RSpec syntax:
expect { code_that_exits }.to raise_error(SystemExit)
If something is printed to STDOUT and you want to test that too, you can do something like:
context "when -h or --help option used" do
it "prints the help and exits" do
help = %Q(
Usage: my_app [options]
-h, --help Shows this help message
)
ARGV << "-h"
expect do
output = capture_stdout { my_app.execute(ARGV) }
expect(output).to eq(help)
end.to raise_error(SystemExit)
ARGV << "--help"
expect do
output = capture_stdout { my_app.execute(ARGV) }
expect(output).to eq(help)
end.to raise_error(SystemExit)
end
end
Where capture_stdout
is defined as seen in
https://stackoverflow.com/questions/11349270/test-output-to-command-line-with-rspec/11349621#11349621.
Update: Consider using RSpec's output
matcher instead of capture_stdout
Solution 3 - Ruby
Thanks for the answer Markus. Once I had this clue I could put together a nice matcher for future use.
it "should exit cleanly when -h is used" do
lambda { ::MyGem::CLI.execute( StringIO.new, ["-h"]) }.should exit_with_code(0)
end
it "should exit with error on unknown option" do
lambda { ::MyGem::CLI.execute( StringIO.new, ["--bad-option"]) }.should exit_with_code(-1)
end
To use this matcher add this to your libraries or spec-helpers:
RSpec::Matchers.define :exit_with_code do |exp_code|
actual = nil
match do |block|
begin
block.call
rescue SystemExit => e
actual = e.status
end
actual and actual == exp_code
end
failure_message_for_should do |block|
"expected block to call exit(#{exp_code}) but exit" +
(actual.nil? ? " not called" : "(#{actual}) was called")
end
failure_message_for_should_not do |block|
"expected block not to call exit(#{exp_code})"
end
description do
"expect block to call exit(#{exp_code})"
end
end
Solution 4 - Ruby
There's no need for custom matchers or rescue blocks, simply:
expect { exit 1 }.to raise_error(SystemExit) do |error|
expect(error.status).to eq(1)
end
I'd argue that this is superior because it's explicit and plain Rspec.
Solution 5 - Ruby
Its not pretty, but I've been using this:
begin
do_something
rescue SystemExit => e
expect(e.status).to eq 1 # exited with failure status
# or
expect(e.status).to eq 0 # exited with success status
else
expect(true).eq false # this should never happen
end
Solution 6 - Ruby
After digging, I found this.
My solution ended up looking like this:
# something.rb
class Something
def initialize(kernel=Kernel)
@kernel = kernel
end
def process_arguments(args)
@kernel.exit
end
end
# something_spec.rb
require 'something'
describe Something do
before :each do
@mock_kernel = mock(Kernel)
@mock_kernel.stub!(:exit)
end
it "should exit cleanly" do
s = Something.new(@mock_kernel)
@mock_kernel.should_receive(:exit)
s.process_arguments(["-h"])
end
end
Solution 7 - Ruby
I had to update the solution @Greg provided due to newer syntax requirements.
RSpec::Matchers.define :exit_with_code do |exp_code|
actual = nil
match do |block|
begin
block.call
rescue SystemExit => e
actual = e.status
end
actual and actual == exp_code
end
failure_message do |block|
"expected block to call exit(#{exp_code}) but exit" +
(actual.nil? ? " not called" : "(#{actual}) was called")
end
failure_message_when_negated do |block|
"expected block not to call exit(#{exp_code})"
end
description do
"expect block to call exit(#{exp_code})"
end
supports_block_expectations
end
Solution 8 - Ruby
Just looking for exit status code of a command.
If all you're doing is testing the exit status code of a command, you can do something like this:
describe Something do
it "should exit without an error" do
expect( system( "will_exit_with_zero_status_code" ) ).to be true
end
it "should exit with an error" do
expect( system( "will_exit_with_non_zero_status_code" ) ).to be false
end
end
This works because system
will return:
true
when the command exits with a "0" status code (i.e. no errors).false
when the command exits with a non 0 status code (i.e. error).
And if you want to mute the output of the system
command from your rspec documentation output, you can redirect it like so:
system( "will_exit_with_zero_status_code", [ :out, :err ] => File::NULL )
Solution 9 - Ruby
I've used @Greg's solution for years now, but I've modified it to work with RSpec 3+.
I've also tweaked it so that the code now optionally checks for an exit status, which I find much more flexible.
Usage
expect { ... }.to call_exit
expect { ... }.to call_exit.with(0)
expect { ... }.to_not call_exit
expect { ... }.to_not call_exit.with(0)
Source
RSpec::Matchers.define :call_exit do
actual_status = nil
match do |block|
begin
block.call
rescue SystemExit => e
actual_status = e.status
end
actual_status && (expected_status.nil? || actual_status == expected_status)
end
chain :with, :expected_status
def supports_block_expectations?
true
end
failure_message do |block|
expected = 'exit'
expected += "(#{expected_status})" if expected_status
actual = nil
actual = "exit(#{actual_status})" if actual_status
"expected block to call `#{expected}` but " +
(actual.nil? ? 'exit was never called' : "`#{actual}` was called")
end
failure_message_when_negated do |block|
expected = 'exit'
expected += "(#{expected_status})" if expected_status
"expected block not to call `#{expected}`"
end
description do
expected = 'exit'
expected += "(#{expected_status})" if expected_status
"expect block to call `#{expected}`"
end
end