How can I validate exits and aborts in RSpec?

RubyRspecMocking

Ruby 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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestioncfedukeView Question on Stackoverflow
Solution 1 - RubyMarkus StraussView Answer on Stackoverflow
Solution 2 - RubyDennisView Answer on Stackoverflow
Solution 3 - RubyGregView Answer on Stackoverflow
Solution 4 - RubythisismydesignView Answer on Stackoverflow
Solution 5 - RubyKrisView Answer on Stackoverflow
Solution 6 - RubycfedukeView Answer on Stackoverflow
Solution 7 - RubyDavid NedrowView Answer on Stackoverflow
Solution 8 - RubyJoshua PinterView Answer on Stackoverflow
Solution 9 - RubyabhchandView Answer on Stackoverflow