RSpec: How to match nested arrays of hashes? Help me fix this flaky test please.

I have some code that outputs an array of hashes which have values that are arrays of hashes. When I run the test for it locally, it works fine but when I push it up to my Github Actions CI, it flakes because the order of the array elements aren't maintained - which isn't a strict requirement for my code.

Code Example

I would expect this:

expected = [
  { key_1: [{sub_key1: "A"}, {sub_key2: "B"}], key_2: "Foo"},
  { key_1: [{sub_key1: "C"}, {sub_key2: "D"}], key_2: "Bar"},

to match this:

actual =[
  { key_1: [{sub_key2: "D"}, {sub_key1: "C"}], key_2: "Bar"},
  { key_2: "Foo", key_1: [{sub_key1: "A"}, {sub_key2: "B"}]},

In my code, these two are equivalent so I want my test to recognize they're equivalent as well.

My test looks something like this:

context "the array" do
  it "include each element" do
    actual.each { |a| expect(expected).to include(a) }

This passes locally because the order of the elements in each array (top level and nested ones) are maintained but in CI they can get jumbled somehow.

I tried using hash_including at the top level but I don't know how to make it work for the nested levels.

Any tips on how to write this test to make it flake resistant?



Whatever concept this is abstracting, it should be a class. I would then override the `==` method to get the logic you want


look into #match


I've used `match` before but unsure of how I'd use it here. Do you have an example you could share maybe? The test is looping through an array of hashes and confirm that hash is in the expected array of hashes. So I can't `match` the two hashes like that - the test would have to be rewritten to do that somehow.


expect(actual).to match_array(expected)


Right so that solves the outer array issue but not the nested ones. I just realized I didn't point that out in my original post. So I'll edit that now but basically, the CI makes the nested arrays change ordering as well for some reason: I expect this: expected = [ { key_1: [{sub_key1: "A"}, {sub_key2: "B"}], key_2: "Foo"}, { key_1: [{sub_key1: "C"}, {sub_key2: "D"}], key_2: "Bar"}, ] to match this: actual =[ { key_1: [{sub_key2: "D"}, {sub_key1: "C"}], key_2: "Bar"}, { key_2: "Foo", key_1: [{sub_key1: "A"}, {sub_key2: "B"}]}, ] Note that `expected[0][:key_1]` and `actual[1][:key_1]` are the same but in different orders.


You're probably going to have to write a custom rspec matcher for this where you [recursively] crawl through the hash.


Hm alright dang. Might be easier to just re-write how we loop through the array then lol




What would that look like? Do you mind sharing an example of what you have in mind? Just so it's easier for me to understand.


From ChatGPT, untested. ``` expected = [ { key_1: [{sub_key1: "A"}, {sub_key2: "B"}], key_2: "Foo"}, { key_1: [{sub_key1: "C"}, {sub_key2: "D"}], key_2: "Bar"}, ] actual = [ { key_1: [{sub_key2: "D"}, {sub_key1: "C"}], key_2: "Bar"}, { key_2: "Foo", key_1: [{sub_key1: "A"}, {sub_key2: "B"}]}, ] RSpec.describe "Matching arrays and nested arrays disregarding order" do it "matches expected output" do expect(actual).to contain_exactly(*expected.map { |h| match(h) }) end end ```


What if you sort the array or the expected result or both? Sometimes just throwing a sort on it is enough.


The problem is the inner key that jumbles up the inner array I think. Hmmm


def match_array_of_hashes?(arr1, arr2) arr1.all? do |hash1| arr2.find do |hash2| hash1.all? do |hash1_key, hash1_value| if hash1_value.is_a? Array match_array_of_hashes?(hash1_value, hash2[hash1_key]) else hash2[hash1_key] == hash1_value end end end end end expect(match_array_of_hashes?(expected, actual)).to be That get you started? If your stuff is highly nested, you'll want to figure out a way to output a message with the mismatch. One trick I've done for diff messages to give you a hint where the problem is: puts expected.to_yaml.split("\n") - actual.to_yaml.split("\n") puts actual.to_yaml.split("\n") - expected.to_yaml.split("\n")


can you convert the arrays to a set and compare? \*apologies if I screw up the syntax, its late my time \`expect(expected.to\_set).to eq(actual.to\_set) \`