James Faulcon
James Faulcon

Reputation: 733

How do I compare two hashes in RSpec?

I have two hashes h1 and h2 that I'd like to compare in RSpec.

I want to check that the elements of h1 are the same as h2 after some transformation, which we'll call f. That is, I want to verify that for every key k in h1, h1[k] == f(h2[k]).

For example, if all the values in h2 are twice as big as the corresponding values in h1, then I want to check that for every key k in h1, h2[k] == h1[k] * 2.

What's the right way to do this in RSpec? Currently I do:

h1 = ...
expect(
  h2.all? { |k,v|
    v == f(h1[k])
  }
).to be true

but that seems clunky.

Upvotes: 12

Views: 27740

Answers (6)

p.matsinopoulos
p.matsinopoulos

Reputation: 7810

We are using hashdiff to deal with this problem in RSpecs.

We have developed a custom matcher:

RSpec::Matchers.define :match_hash do |expected|
  match do |actual|
    Hashdiff.best_diff(expected, actual).blank?
  end
  failure_message do |actual|
    "expected that #{actual} would match #{expected}, but it has these differences: #{Hashdiff.best_diff(expected, actual)}"
  end
end

and we use it like this: expect(actual_hash).to match_hash(expected_hash)

Upvotes: 3

fabriciofreitag
fabriciofreitag

Reputation: 2883

if anyone comes looking for a better diff for big nested hashes, I've came up with this:

RSpec::Matchers.define :be_a_hash_like do |expected_hash|
  match do |actual_hash|
    matching_results = actual_hash == expected_hash
    unless matching_results
      system(
        "git diff $(echo '#{JSON.pretty_generate(expected_hash)}' | git hash-object -w --stdin) " +
                 "$(echo '#{JSON.pretty_generate(actual_hash)}' | git hash-object -w --stdin) --word-diff",
        out: $stdout,
        err: :out
      )
    end
    matching_results
  end

  failure_message { 'Look at the Diff above! ^^^' }
end

# then use:
expect(actual).to be_a_hash_like(expected)

original gist: https://gist.github.com/fabriciofreitag/b7458725ffef08eaf5ea541c95385a92

Output example: enter image description here

Upvotes: 4

Andy Jones
Andy Jones

Reputation: 1104

How about:

h2 = f(h1)

expect(h2.keys).to eq(h1.keys)  # I assume you want this, too

h1.keys.each do |k|
  expect(h2[k]).to eq(h1[k] * 2) 
end

A little more long-winded, but maybe more readable?

Upvotes: 1

Aaron K
Aaron K

Reputation: 6961

Sounds like what you are testing is the transformation. I would consider writing something like this:

it "transforming something does something" do
  base_data = { k1: 1, k2: 2 }
  transformed_data = base_data.each_with_object({}) { |(k, v), t|
    t[k] = f(v)
  }

  expect(transformed_data).to eq(
    k1: 2,
    k2: 4,
  )
end

To me the description clearly states what we are expecting. Then I can easily see from the test what the input is and the expected output. Also, this leverages the hash matcher which will provide a nice diff of the two hashes on a failure:

expected: {:k1=>2, :k2=>4}
     got: {:k1=>1, :k2=>4}

(compared using ==)

Diff:
@@ -1,3 +1,3 @@
-:k1 => 2,
+:k1 => 1,
 :k2 => 4,

Though I would question what the key-value relationship means. Are these simply test cases you are trying to run through? If so, I'd just make each a unique tests. If there is something more to it, then I may question why the transform method isn't provided the hash to start with.

Upvotes: 7

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

For an exact equality:

expect(h1).to eq h2.map { |k, v| [k, f(v)] }.to_h

Upvotes: 0

Ilya
Ilya

Reputation: 13487

h1.each do |k, v|
  expect(v).to eq(f(h2[k]))
end

As for me, it seems more readable.

Upvotes: 1

Related Questions