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.
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