How can I validate exits and aborts in RSpec?

Viewed 14869

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).

9 Answers

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 )

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
Related