Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to spec methods that exit or abort

Tags:

ruby

rspec

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?

like image 556
Jimmy Avatar asked Aug 11 '11 01:08

Jimmy


3 Answers

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
like image 31
ocodo Avatar answered Nov 20 '22 16:11

ocodo


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
like image 200
David Grayson Avatar answered Nov 20 '22 16:11

David Grayson


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)>'
like image 2
zetetic Avatar answered Nov 20 '22 17:11

zetetic