Created
June 5, 2013 10:14
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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