1

In my RSpec tests I often have the challenge to compare deep nested hashes like this

{ foo: ["test", { bar: [1,2,3] }] }

The values [1,2,3] are read from a DB where the order is not guaranteed, nor do I care about the order. So when I compare the two hashes in my test, I always have to make sure on both sides that the order is enforced:

# my_class.rb
class MyClass
  def self.data
    read_db.sort
  end
end

expected_data = { foo: ["test", { bar: [1,2,3].sort }] }
expect(MyClass.data).to eq(expected_data)

I really dislike the fact, that I have to alter my production code only because of my test env.

I could of course stop comparing the whole hash and focus on the single keys, and therefore could remove the custom sorting inside my production code:

actual_data = MyClass.data
expect(actual_data.fetch(:foo)[0]).to eq("test")
expect(actual_data.fetch(:foo)[1].fetch(:bar)).to match_array([1,2,3])

But this makes the whole spec pretty complicated and hard to read.

So I thought about creating a custom "unordered array" class Bag that, when it get's compared, ignores the order:

class Bag < Array
  def eql?(other)
    sort_by(&:hash) == other.sort_by(&:hash)
  end
  alias == eql?
end

But this works only, when the Bag class is on the left side of the comparison:

expect(Bag.new([1, "2"])).to eq(["2", 1])

 1 example, 0 failures

But that's usually not the case, as the expected value in a test should be inside expect(...), which represents the values from the DB:

expect(["2", 1]).to eq(Bag.new([1, "2"]))

 Failure/Error: expect(["2", 1]).to eq(Bag.new([1, "2"]))
   expected: [1, "2"]
        got: ["2", 1]
   (compared using ==)

 1 example, 1 failure

The reason behind this is, that Array#== is called and not my custom Bag#== method.

I looked into the docs (https://devdocs.io/ruby~3.2/array#method-i-3D-3D) where it states

Returns true if both array.size == other_array.size and for each index i in array, array[i] == other_array[i]:

But here I came to a stop, as I wasn't able to figure out, how to implement the fetching of a value for a specific index. I tried implementing Bar#[] and Bar#fetch but they aren't called when comparing objects.

Maybe it's not possible at all because array calls some low level C function that can't be overriden. But maybe someone knows a solution.

mechnicov
  • 12,025
  • 4
  • 33
  • 56
23tux
  • 14,104
  • 15
  • 88
  • 187

2 Answers2

3

I would use match with a nested match_array like this:

it 'matches when nested arrays having different sorting' do
  data = { foo: ['test', { bar: [3, 2, 1] }] }

  expect(data).to match(
    { foo: ['test', bar: match_array([1, 2, 3])] }
  )
end
spickermann
  • 100,941
  • 9
  • 101
  • 131
1

I could of course stop comparing the whole hash

You can continue to compare whole hash. RSpec is very powerful tool

RSpec allows you to use its matchers DSL directly in the expected code

Therefore why don't use this feature?

Additionally, there is a feature, which detailed documentation I did not find:

This is using match for hashes

RSpec.describe do
  let(:hsh) { { foo: ["test", { bar: [1,2,3] }] } }

  it 'check nested hashes well' do
    expect(hsh).to match(foo: contain_exactly("test", match(bar: contain_exactly(1, 2, 3))))
  end
end

Here match and contain_exactly are used in the expected output. Of course you can use match_array like match_array([1, 2, 3]) or any other matchers

mechnicov
  • 12,025
  • 4
  • 33
  • 56