Skip to content

Instantly share code, notes, and snippets.

@danlucraft
Created June 5, 2013 10:14
Show Gist options
  • Save danlucraft/5712925 to your computer and use it in GitHub Desktop.
Save danlucraft/5712925 to your computer and use it in GitHub Desktop.
Calling return in a block can have slightly subtle consequences. Here are some tests with comments that show how it works.
require 'minitest/autorun'
# Show what return in a block does to return values and code after the block is run
class TestReturn < Minitest::Unit::TestCase
def setup
@post_code_run = false
end
# Method under test
# Calls a block, then returns its argument times 2
def yields_and_returns_arg(return_value)
yield
@post_code_run = true
return_value*2
end
# Helper to call the above method with a return in the block
def call_with_return_in_block(return_value_normal, return_value_block)
yields_and_returns_arg(return_value_normal) { return return_value_block }
end
# Tests
# Ordinarily the code after the yield is run and the return value is
# set by the last statement in the function
def test_block_ordinary_return
ret = yields_and_returns_arg("xxx") { 123 }
assert @post_code_run
assert_equal "xxxxxx", ret
end
# BUT if your block calls return, then no code after the yield is run
# and the return value of your function is automatically the return value
# of the block.
def test_block_changes_return
ret = call_with_return_in_block("xxx", 123)
assert !@post_code_run
assert_equal 123, ret
end
end
# Now let's look at how this all interacts with rescue, else and ensure blocks
#
# An else block is run if (there is no error thrown by the function AND if the
# block did not return).
# BUT does an else block get run if a block calls return? Let's find out.
class TestElseWithReturn < Minitest::Unit::TestCase
def setup
@error_block_called = false
@else_block_called = false
@ensure_block_called = false
end
# Method under test
def yields_and_returns_arg(return_value)
yield
return_value*2
rescue Object => e
@error_block_called = true
error_value = "error_return_value"
else
@else_block_called = true
"else_return_value"
ensure
@ensure_block_called = "ensure_block_value(error_value = #{error_value})"
end
# Helper
def call_with_return_in_block(return_value_normal, return_value_block)
yields_and_returns_arg(return_value_normal) { return return_value_block }
end
# Tests
# Ordinarily both else and ensure blocks are run, and the return value
# is the value from the else block.
def test_ordinary_return_value
ret = yields_and_returns_arg("xxx") { 123 }
assert_equal "else_return_value", ret
assert !@error_block_called
assert @else_block_called
assert @ensure_block_called
end
# If there is an error, the error block is run, the else block is not, and the
# return value is the value of the error block.
def test_error_block_catches_and_returns_rescue_value
ret = yields_and_returns_arg("xxx") { raise }
assert_equal "error_return_value", ret
assert @error_block_called
assert !@else_block_called
assert @ensure_block_called
end
# If there is an error in the block: the else block is not run, the error block
# is run, and the return value is the value of the error block.
#
# Additionally, when the ensure block is run (as it always is), it has access to
# variables set in the ensure block, which can be useful.
def test_ensure_runs_with_error_and_has_access_to_error_variables_and_returns_error_value
ret = yields_and_returns_arg("xxx") { raise }
assert_equal "error_return_value", ret
assert @error_block_called
assert !@else_block_called
assert_equal "ensure_block_value(error_value = error_return_value)", @ensure_block_called
end
# When the block returns, the error block is not run, the *else block is also not run*,
# and the return value is the return value of the block. Variables set in the error block
# (which did not run) are set to nil in the ensure block.
def test_else_doesnt_run_when_block_returns_and_returns_block_return_value
ret = call_with_return_in_block("xxx", 123)
assert_equal 123, ret
assert !@error_block_called
assert !@else_block_called
assert_equal "ensure_block_value(error_value = )", @ensure_block_called
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment