How to spec methods that exit or abort - ruby

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?

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

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

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

Related

rspec exits when program exits

When RSpec comes across exit in my code, it also exits and no further tests are run. Here is a distilled example:
class Parser
def initialize(argv)
#options = {}
optparse(argv)
end
def optparse(argv)
OptionParser.new do |opts|
opts.on_tail('-h', '--help', 'Show this message') do
puts opts
exit
end
end.parse!(argv, into: #options)
end
end
RSpec.describe Parser do
context 'when -h is passed' do
it 'exits cleanly' do
expect(described_class.new(['-h'])).to raise(SystemExit)
end
end
context 'when --help is passed' do
it 'exits cleanly' do
expect(described_class.new(['--help'])).to raise(SystemExit)
end
end
end
I've also tried exit_with_code(0) and multiple forms of writing these two tests to get the 2nd one to run. Any suggestions?
If I replace
expect(described_class.new(['--help']))
with
expect { described_class.new(['--help']) }
it works.
And since I'm also doing other tests on the same command (in addition to the SystemExit check), I've had to explicitly rescue it and continue on.
it "other test" do
begin
parser.new(args)
rescue SystemExit
end
expect(...)
end
This pattern is kinda ugly, but works. The suppressed exception will require a rubocop override if you use rubocop, but I got that pattern from rubocop's own rspec tests, so ¯\_(ツ)_/¯

(RSpec) How do I check if an object is created?

I want to test if expected exception handling is taking place in the following Ruby code through RSpec. Through testing I realized that I cannot use the raise_error matcher to test if the exception was raised, after rescuing it.
So, now I want to test whether objects of CustomError and StandardError are created to see if the error was raised as expected.
test.rb
module TestModule
class Test
class CustomError < StandardError
end
def self.func(arg1, arg2)
raise CustomError, 'Inside CustomError' if arg1 >= 10 && arg2 <= -10
raise StandardError, 'Inside StandardError' if arg1.zero? && arg2.zero?
rescue CustomError => e
puts 'Rescuing CustomError'
puts e.exception
rescue StandardError => e
puts 'Rescuing StandardError'
puts e.exception
ensure
puts "arg1: #{arg1}, arg2: #{arg2}\n"
end
end
end
test_spec.rb
require './test'
module TestModule
describe Test do
describe '#func' do
it 'raises CustomError when arg1 >= 10 and arg2 <= -10' do
described_class.func(11, -11)
expect(described_class::CustomError).to receive(:new)
end
end
end
end
When I run the above code I get the following error
Failures:
1) TestModule::Test#func raises CustomError when arg1 >= 10 and arg2 <= -10
Failure/Error: expect(described_class::CustomError).to receive(:new)
(TestModule::Test::CustomError (class)).new(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
# ./test_spec.rb:8:in `block (3 levels) in <module:TestModule>'
The idea was that if CustomError is being raised, it's obejct must be created using new and I can test that using RSpec. However, as you can see, this isn't working.
What am I missing?
Is there a better approach?
What you are trying to do is to test implementation details (what happens inside the method), and usually it's a bad idea. If a particular path leads to an exception but you want this exception to be swallowed - test that.
consider
def weird_division(x, y)
x/y
rescue ZeroDivisionError => e
return "Boom!"
end
No need to test that ZeroDivisionError has been created, that's an implementation detail (akin to testing private methods). Test behavior that is "visible" from the outside.
expect(weird_division(1/0)).to return "Boom!"
Because you might change the implementation:
def weird_division(x, y)
return "Boom!" if y == 0
x/y
end
And your tests would start failing, even though the method behaves the same.
I see two possibilities.
If you want to continue with your assertion you can use have_received instead of receive. This will allow you to keep you assertion after you act.
expect(described_class::CustomError).to have_received(:new)
As mentioned in comments test your puts. Which is ultimately the way that you check if your method does what you want.
it 'puts the output' do
expect do
described_class.func(11, -11)
end.to output('Some string you want to print').to_stdout
end

Detecting and handling NonLocalExit from ruby

I have this code that can trigger in production the "Non local exit detected!" branch. I can't understand how that can happen, since even a return will trigger a NonLocalExit exception. Even a throw will trigger an exception.
Is there any way to have exception_raised and yield_returned both false?
def transaction
yield_returned = exception_raised = nil
begin
if block_given?
result = yield
yield_returned = true
puts 'yield returned!'
result
end
rescue Exception => exc
exception_raised = exc
ensure
if block_given?
unless yield_returned or exception_raised
puts 'Non local exit detected!'
end
end
end
end
transaction do
puts 'good block!'
end
transaction do
puts 'starting transaction with block with return'
return
puts 'this will not show'
end
Output:
good block!
yield returned!
starting transaction with block with return
I want to somehow output 'Non local exit detected!'. I know this happens in production, but I can't make it happen in development. Tried it with a return and with a throw, but they both raise an exception. Any other way?
The problem was with my reproduction, and returning from the top-most level in Ruby. You can return from a method, where there's a stack, but if you return from Kernel, you'll get a LocalJumpError.
Assuming the previous method transaction(). In matters in what context you call return:
def lol
transaction do
puts 'Block in method: starting transaction with block with return'
return
puts 'this will not show'
end
end
lol()
transaction do
puts 'block in Kernel: starting transaction with block with return'
return
puts 'this will not show'
end
Output:
$ ruby local_jump_error.rb
# Running from method:
Block in method: starting transaction with block with return
yield_returned=nil, exception_raised=nil
Non local exit detected!
# Running without method:
block in Kernel: starting transaction with block with return
yield_returned=nil, exception_raised=#<LocalJumpError: unexpected return>
local_jump_error.rb:45: unexpected return (LocalJumpError)
from local_jump_error.rb:6:in `transaction'
from local_jump_error.rb:43
I don't know your level with Ruby, but to exit from a block you must use break instead of return. In this case, break also accepts a value in the same way that return does, meaning that a break will conceptually assign the variable result with the value from break.
In case of a throw, it will raise an exception, so it will always unwind the call stack until if finds a rescue statement, thus it will run the code exception_raised = exc.
You can fine tune the rescue to use LocalJumpError instead of Exception to only catch blocks that have return in them. All other exception types will then not stop at that rescue.
I wonder if you meant to write:
if block_given?
if yield_returned.nil? && exception_raised
puts 'Non local exit detected!'
end
end
If you make this change, the code will produce the 'Non local exit detected!', for the second method call of #transaction with the return.
When you wrote unless yield_returned or expection_raised, the if clause would get evaluated only when both the variables would have been false. And as I understand that is not possible.
And as a side note, as was suggested in the other answer, one should not rescue Exception, LocalJumpError should be enough.

New to ruby and trying to fix the error from rspec

Hi I need to know how to do the following
rspec code:
2) WebServer::Htaccess#authorized? for valid-user with valid credentials returns true
Failure/Error: expect(htaccess_valid_user.authorized?(encrypted_string)).to be_true
ArgumentError:
wrong number of arguments calling `authorized?` (1 for 0)
# ./spec/lib/config/htaccess_spec.rb:82:in `(root)'
# ./spec/lib/config/htaccess_spec.rb:44:in `stub_htpwd_file'
# ./spec/lib/config/htaccess_spec.rb:41:in `stub_htpwd_file'
# ./spec/lib/config/htaccess_spec.rb:40:in `stub_htpwd_file'
# ./spec/lib/config/htaccess_spec.rb:81:in `(root)'
Here is the spec.rb file
let(:htaccess_valid_user) { WebServer::Htaccess.new(valid_user_content) }
let(:htaccess_user) { WebServer::Htaccess.new(user_content) }
describe '#authorized?' do
context 'for valid-user' do
context 'with valid credentials' do
it 'returns true' do
stub_htpwd_file do
expect(htaccess_valid_user.authorized?(encrypted_string)).to be_true
end
end
end
context 'with invalid credentials' do
it 'returns false' do
stub_htpwd_file do
expect(htaccess_valid_user.authorized?(encrypted_string('bad user'))).not_to be_nil
expect(htaccess_valid_user.authorized?(encrypted_string('bad user'))).to be_false
end
end
end
end
I am new to ruby TDD, and all I have in my file right now is
def authorized?
end
I am fluent in Node.js but this is completely new to me.
Please help.
It's right there in the error message.
ArgumentError:
wrong number of arguments calling `authorized?` (1 for 0)
You've passed arguments to the authorized? method.
expect(htaccess_valid_user.authorized?(encrypted_string)).to be_true
^^^^^^^^^^^^^^^^^^
But authorized? takes no arguments.
def authorized?
end
Unlike Javascript, Ruby will check you passed in the right number of arguments. If you specify no argument list, the default is to enforce taking no arguments. Add some.
def authorized?(authorization)
end

Rspec should_receive and abort

I have following code:
class Init
def initialize(global_options, options, args)
abort "Key file must be given!" if (key_file = args.first).nil?
begin
#secret = File.read(key_file)
rescue
abort "Cannot read key file #{key_file}"
end
stdout, stderr, status = Open3.capture3("git status -uno --porcelain")
#...
and following specs for it:
describe Rgc::Init do
context :initialize do
it 'should abort when no key file given' do
Rgc::Init.any_instance.should_receive(:abort)
.with("Key file must be given!")
Rgc::Init.new({}, {}, [])
end
end
end
And I get following output:
Failure/Error: Rgc::Init.new({}, {}, [])
#<Rgc::Init:0x0000000157f728> received :abort with unexpected arguments
expected: ("Key file must be given!")
got: ("Cannot read key file ")
should_receive method somehow block abort to take a place. How to fix the spec to check that app has been aborted and with specific message?
Your two expectations need to be treated as separate things. First, as you noticed, abort is now stubbed and therefore doesn't actually abort execution of your code -- it's really just acting like a puts statement now. Because of this, abort is being called twice: once with your expected message, and then again within your begin block. And if you add { abort } to the end of your expectation, it will actually abort, but that will also abort your test suite.
What you should do is use a lambda and make sure the abort is called:
lambda { Rgc::Init.new({}, {}, []) }.should raise_error SystemExit
abort prints the message you give it to stderr. To capture that, you can add a helper to temporarily replace stderr with a StringIO object, which you can then check the contents of:
def capture_stderr(&block)
original_stderr = $stderr
$stderr = fake = StringIO.new
begin
yield
ensure
$stderr = original_stderr
end
fake.string
end
it 'should abort when no key file given' do
stderr = capture_stderr do
lambda { Rgc::Init.new({}, {}, []) }.should raise_error SystemExit
end
stderr.should == "Key file must be given!\n"
end
(Credit to https://stackoverflow.com/a/11349621/424300 for the stderr replacement)

Resources