A failing Ruby test often hands you two nearly identical hashes and leaves you to find the difference. This guide explains how Ruby produces that output, how to read it, the traps that waste the most time, and how to debug it systematically instead of squinting.
Ruby's built-in inspect output is designed to be unambiguous, not
readable. When a test framework like Minitest reports a failed assertion on two
hashes, it prints each hash on a single line in insertion order, with no
wrapping and no alignment. For a small hash that is fine. For a real-world
record with twenty keys, deep nesting, and long string values, you get two
walls of text that differ in one place you cannot see.
Three properties of Ruby hashes make this worse than it needs to be:
"a" => 1), symbol keys (:a => 1),
and the modern shorthand (a: 1) all at once, depending on where
the data came from.
"3" and
3 look almost the same in a wall of output, but one is a string
and one is an integer, and that difference is frequently the whole bug.
When an assert_equal on two hashes fails, Minitest prints something
shaped like this:
Expected: {"id"=>1, "name"=>"Alice", "role"=>"admin", "active"=>true}
Actual: {"id"=>1, "name"=>"Alice", "role"=>"editor", "active"=>true} Read it in this order:
role, which went from "admin" to "editor".
1 versus 1.0, or
:admin versus "admin", are the usual suspects.
If your two hashes come from different sources, a database row versus a hand-written fixture, their keys may appear in different orders. A naive character-by-character comparison will report dozens of differences when the data is actually identical. The fix is to sort keys before comparing, which is the first thing RubyHash does for you. Sorting turns "everything changed" back into "nothing changed," so the one genuine difference, if any, stands out.
In Ruby, :name and "name" are different keys.
JSON.parse gives you string keys, form params arrive as strings,
and code you wrote by hand usually uses symbols. When the two meet, you get a
hash that looks correct but returns nil on lookup. In a diff, the
two hashes look nearly identical because the only difference is a colon versus a
pair of quotes on every key.
A value that changes type, nil to a string, an integer to a float,
a string to a nested hash, is one of the most common real bugs and one of the
hardest to spot by eye. "0" and 0 sit one character
apart in a wall of output. RubyHash flags type changes explicitly with a badge,
so a nil that became an empty string does not slip past you.
When a hash contains hashes that contain arrays that contain more hashes, the single-line output becomes nearly impossible to parse mentally. The closing braces stack up and you lose track of which level you are on. A structured, indented, side-by-side view solves this by giving every level its own line and indentation.
When a hash assertion fails, resist the urge to immediately change the test to make it pass. Work through this instead:
role field
is wrong," which is a question you can actually answer.
To read hash output fluently, it helps to recognize every form Ruby uses:
{"a" => 1} a string key with the hashrocket operator{:a => 1} a symbol key with the hashrocket operator{a: 1} the modern shorthand, equivalent to :a => 1nil, true, false Ruby literals that map to JSON null, true, false[1, 2, 3] an array, which can appear as a value at any depthRubyHash understands all of these and normalizes them so the two sides of your diff are compared on equal footing, regardless of which syntax each side happened to use.
The blog goes deeper on the Ruby features behind these diffs. A few good starting points:
The next time a test drops two hashes in your terminal, do not read them there. Copy both lines, paste them into the tool on the home page, and let RubyHash sort the keys, highlight the one value that changed, and flag any type differences. The whole point is to turn a ten-minute squint into a two-second glance.
New articles on Ruby, Rails, testing, and tooling, delivered to your inbox.