Techioz Blog

RSpec でネストされたハッシュをテストする場合のランダムな順序の配列のマッチング

概要

RSpec テストでは、次のような深くネストされたハッシュを比較するという課題がよくあります。

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

値 [1,2,3] は、順序が保証されていない DB から読み取られます。また、順序も気にしません。したがって、テストで 2 つのハッシュを比較するときは、必ず両側で順序が適用されていることを確認する必要があります。

# 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)

私は、テスト環境のためだけに本番コードを変更しなければならないという事実が本当に嫌いです。

もちろん、ハッシュ全体の比較をやめて単一のキーに焦点を当てることもできるため、運用コード内のカスタム並べ替えを削除することもできます。

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])

しかし、これにより仕様全体がかなり複雑になり、読みにくくなります。

そこで、比較時に順序を無視するカスタムの「順序なし配列」クラス Bag を作成することを考えました。

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

ただし、これは Bag クラスが比較の左側にある場合にのみ機能します。

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

 1 example, 0 failures

ただし、テストの期待値は DB からの値を表す Expect(…) 内にある必要があるため、通常はそうではありません。

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

この背後にある理由は、カスタム Bag#== メソッドではなく、Array#== が呼び出されるからです。

ドキュメント(https://devdocs.io/ruby~3.2/array#method-i-3D-3D)を調べました。

しかし、ここで、特定のインデックスの値のフェッチを実装する方法がわからず、行き止まりになってしまいました。 Bar#[] と Bar#fetch を実装しようとしましたが、オブジェクトを比較するときにこれらは呼び出されません。

おそらく、配列がオーバーライドできない低レベルの C 関数を呼び出すため、まったく不可能である可能性があります。しかし、もしかしたら誰かが解決策を知っているかもしれません。

解決策

次のように、ネストされた match_array で match を使用します。

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