[ACCEPTED]-How can I validate exits and aborts in RSpec?-mocking

Accepted answer
Score: 30

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

0

Score: 22

Using the new RSpec syntax:

expect { code_that_exits }.to raise_error(SystemExit)

If something 4 is printed to STDOUT and you want to test 3 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 2 capture_stdout is defined as seen in Test output to command line with RSpec.

Update: Consider using 1 RSpec's output matcher instead of capture_stdout

Score: 19

Thanks for the answer Markus. Once I had 3 this clue I could put together a nice matcher 2 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 1 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
Score: 12

There's no need for custom matchers or rescue 2 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 1 because it's explicit and plain Rspec.

Score: 4

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

0

Score: 2

After digging, I found this.

My solution ended up looking 1 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
Score: 1

I had to update the solution @Greg provided 1 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
Score: 0

Just looking for exit status code of a command.

If all you're doing is testing the exit 5 status code of a command, you can do something 4 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 3 if you want to mute the output of the system command 2 from your rspec documentation output, you 1 can redirect it like so:

system( "will_exit_with_zero_status_code", [ :out, :err ] => File::NULL )
Score: 0

I've used @Greg's solution for years now, but 4 I've modified it to work with RSpec 3+.

I've 3 also tweaked it so that the code now optionally 2 checks for an exit status, which I find 1 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

More Related questions