arraysrubyrspecruby-hash

Matching of randomly ordered array when test nested hash with RSpec


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.


Solution

  • 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