← Back to blog

Rails Test Failing on a Hash Assertion? A Debugging Checklist

· Lachlan Young

It is one of the more maddening Rails test failures: you assert that two hashes are equal, you read both, they look the same, and the test fails anyway. The hashes are not actually identical; the difference is just hiding. Here is a checklist of the usual culprits, roughly in order of how often they are the cause.

1. Symbol keys versus string keys

This is the most common one by a wide margin. In Ruby, :name and "name" are different keys, so two hashes with the same values but different key types are not equal.

expected = { name: "Alice", role: "admin" }          # symbol keys
actual   = { "name" => "Alice", "role" => "admin" }  # string keys

expected == actual   # => false

This bites constantly in Rails because data crosses type boundaries. JSON.parse returns string keys. params uses a special hash that accepts both. A serializer might emit strings while your fixture uses symbols. When a symbol-keyed hash meets a string-keyed one, they look the same printed out but compare as unequal.

How to check: look closely at whether one side has :key => and the other has "key" =>. If so, normalize one side. transform_keys(&:to_sym) or transform_keys(&:to_s) will line them up, or use deep_symbolize_keys / deep_stringify_keys for nested data.

2. A type change in a value

The values look equal but are different types. 1 versus 1.0, "3" versus 3, :admin versus "admin". These pass the eye test and fail the equality test.

expected = { count: 5 }
actual   = { count: 5.0 }   # an integer became a float somewhere

expected == actual   # => false, even though 5 and 5.0 look the same

How to check: in Rails this often comes from the database (a decimal column returns a BigDecimal, an integer column an Integer) or from JSON round-tripping. Confirm the class of each suspicious value with .class.

3. BigDecimal and Time precision

Rails models frequently hold values that look simple but carry hidden precision.

# These print similarly but are not equal:
BigDecimal("5.0") == 5.0        # => can surprise you
Time.now == Time.now            # => false, sub-second differences

Money columns come back as BigDecimal, timestamps as ActiveSupport::TimeWithZone with microsecond precision. Two timestamps that print to the same second can differ underneath.

How to check: if the differing key is a money amount, a date, or a time, suspect precision. Compare with an explicit tolerance or round both sides before asserting.

4. Ordering, if you are comparing as arrays or strings

Plain Hash#== does not care about key order, so ordering alone will not fail a hash assertion. But if somewhere you converted to arrays (.to_a) or to strings (.to_s, .inspect) before comparing, order suddenly matters again.

{ a: 1, b: 2 }.to_a == { b: 2, a: 1 }.to_a   # => false (order matters for arrays)

How to check: make sure you are comparing hashes as hashes. If you must compare as arrays, sort both first.

5. An extra or missing key

The two hashes differ by a key that is present on one side only, and it is easy to overlook when scanning for changed values.

expected = { id: 1, name: "Alice" }
actual   = { id: 1, name: "Alice", updated_at: <timestamp> }

expected == actual   # => false (actual has an extra key)

How to check: compare the key sets directly with expected.keys.sort and actual.keys.sort, or expected.keys - actual.keys and the reverse. A serializer that gained a field, or a updated_at that snuck in, shows up immediately.

The fastest way to see which one it is

You can work through this checklist by hand, but the quickest way to find the real cause is to look at a clean diff of the two hashes. Copy both from the test failure and paste them into RubyHash. It sorts the keys, highlights the values that differ, and flags type changes explicitly, which covers items 1, 2, and 5 on this list in a single glance. Instead of guessing which category your failure falls into, you see it: a highlighted :name versus "name", a 5 versus 5.0, or an extra updated_at that should not be there.

It runs entirely in the browser, so test data with real values stays on your machine.

A note on writing assertions that fail clearly

Once you have found the cause, you can often prevent the next confusing failure by asserting more precisely. Instead of comparing whole hashes when you only care about part of them, assert on the specific keys. And when you do compare whole hashes, normalize key types first so a symbol-versus-string mismatch never masquerades as a value difference:

assert_equal expected.deep_stringify_keys, actual.deep_stringify_keys

A failing hash assertion is almost always one of the five causes above. The trick is not memorizing them; it is getting a readable diff so the cause names itself.


Debugging a Rails test failure with mismatched hashes? Try RubyHash, paste your expected and actual hashes for a clean, side-by-side diff.

Enjoyed this post?

Subscribe to get notified when we publish more Ruby and Rails content.