Using Hash Fetch
I fat-finger my code a lot and produce a lot of typos. I do test driven development so it’s not as bad, but what’s annoying is when I typo a hash key and the test blows up with a failure due to a nil
– resulting in a very confusing error message.
The issue
One of my tests look like this:
Given(:instance) { klass.new(payment: payment, signup: signup) }
context "payment data" do
When(:line) { instance.payment_hash }
Then { line[:amount] == 99.0 }
Then { line[:shipment_address] == "999 papaya triangle windsor alabama 06040 united states" }
Then { line[:bundle_ids] == "88 44" }
Then { line[:bundle_names] == "test_bundle_88 test_bundle_44" }
Then { line[:payment_id] == 859017 }
Then { line[:payment_state] == "paid" }
Then { line[:payment_date] == "2015-11-11" }
end
Can you see the typo?
I couldn’t – not immediately. At least, not until I saw the code implementation:
def payment_hash
{
amount: @payment.amount,
shipping_address: address_to_s(@payment.order.shipping_address),
bundle_ids: @payment.order_lines.map {|o| o.bundle.id}.join(' '),
bundle_names: @payment.order_lines.map {|o| o.bundle.name}.join(' '),
payment_id: @payment.id,
payment_state: @payment.state,
payment_date: date_to_excel_string(@payment.created_at),
}
end
Can you see it now?
The soultion
I eventually figured out where the typo was, and was super annoyed that I changed all my hash key access for this test to Hash#fetch
:
Given(:instance) { klass.new(payment: payment, signup: signup) }
context "payment data" do
When(:line) { instance.payment_hash }
Then { line.fetch(:amount) == 99.0 }
Then { line.fetch(:shipping_address) == "999 papaya triangle windsor alabama 06040 united states" }
Then { line.fetch(:bundle_ids) == "88 44" }
Then { line.fetch(:bundle_names) == "test_bundle_88 test_bundle_44" }
Then { line.fetch(:payment_id) == 859017 }
Then { line.fetch(:payment_state) == "paid" }
Then { line.fetch(:payment_date) == "2015-11-11" }
end
Hash#fetch
is way to get the value from a hash, given a hash key. It’s very similar to #[]
with some slight (and in this case, effective) differences.
From the documentation:
fetch(key [, default] ) → obj
fetch(key) {| key | block } → obj
Returns a value from the hash for the given key. If the key can’t be found, there are several options: With no other arguments, it will raise an
KeyError
exception; if default is given, then that will be returned; if the optional code block is specified, then that will be run and its result returned.
h = { "a" => 100, "b" => 200 }
h.fetch("a") #=> 100
h.fetch("z", "go fish") #=> "go fish"
h.fetch("z") { |el| "go fish, #{el}"} #=> "go fish, z"
The following example shows that an exception is raised if the key is not found and a default value is not supplied.
h = { "a" => 100, "b" => 200 }
h.fetch("z")
produces:
prog.rb:2:in `fetch': key not found (KeyError)
from prog.rb:2
The last example is what I gain the most benefit from: it clarifies exactly where I went wrong. In the first code example I gave, the issue was I was using shipment_address
instead of shipping_address
and since they both looked almost the same I initially thought that the value was indeed nil
.
By using Hash#fetch
I completely sidestep the problem of confusing error messages and get a clearer one that tells me the key I’m trying to access does not actually exist.
FAQ
You said you’re doing TDD, but I can see that the typo is in the middle of the test! If you’re really doing TDD then you should have caught the bug at the last line!!
It’s true, I caught the bug at the last line (and catching the typo was a lot easier because I knew exactly where to look). You’ll notice that the hash keys are alphabetically arranged; I moved the typo’d line in the middle as an artistic decision. As Mark Twain famously said: never let the truth get in the way of a good story. :P
How can this be used to avoid
&&
when trying to access a deep nested hash like this:
hash = {foo:{bar: {baz: "hello world"}}}
puts hash[:foo][:bar][:baz] if hash[:foo] && hash[:foo][:bar]
You can use the default
value option during the call:
hash = {foo:{bar: {baz: "hello world"}}}
puts hash.fetch(:foo, {}).fetch(:bar, {}).fetch(:baz, nil)
Or, if you’re using Ruby 2.3 you can use Hash#dig
hash = {foo:{bar: {baz: "hello world"}}}
puts hash.dig(:foo, :bar, :baz)
If you’re not yet using Ruby 2.3 (which is true at the time of this post’s publication) then you can use a gem to add that method call.