Question

Compare Ruby Arrays in Any Order with RSpec match_array

rubytestingrspec

Question

How can I write an RSpec expectation that verifies two Ruby arrays contain exactly the same elements in any order?

The solution must handle duplicate values correctly, unlike converting arrays to sets:

expect(array.to_set).to eq(another_array.to_set)

It also must not require elements to implement <=>, unlike sorting both arrays:

expect(array.sort).to eq(another_array.sort)

Is there a concise RSpec matcher for this comparison?

Short Answer

You will learn to use RSpec's match_array and contain_exactly matchers to compare collections without considering order. These matchers still verify duplicate counts, so they are more precise than comparing sets and do not require sortable elements.

Concept

Array equality in Ruby is order-sensitive:

["red", "blue"] == ["blue", "red"] # false

That is correct when position has meaning, such as a ranked leaderboard or a sequence of steps. But many tests only care about collection contents: which values are present and how many times each occurs.

RSpec provides matchers for this purpose:

  • match_array(expected) checks that the actual array contains the same elements as expected, in any order.
  • contain_exactly(*items) expresses the same idea as a list of expected items.

Both matchers:

  • ignore ordering;
  • require every expected element to be present;
  • reject unexpected elements;
  • preserve duplicate counts;
  • compare elements using RSpec's matching behavior, which supports values and nested matchers.

For example, these arrays match because they contain one "apple" and two "pear" values each:

expect(["pear", "apple", "pear"])
  .to match_array(["apple", "pear", "pear"])

But they do not match if one duplicate is missing. This matters because a set removes duplicates, which can hide bugs in results, inventory quantities, event lists, or API responses.

Mental Model

Think of an array as a bag of objects.

  • Normal array equality (eq) lines objects up in two rows and compares each position.
  • match_array pours both rows into bags, then checks whether the bags hold the same objects in the same quantities.

A set is a different kind of bag: it keeps only one copy of each object. Therefore, it cannot tell the difference between one apple and three apples. match_array can.

Take Quiz

Syntax and Examples

Use match_array when your expected values are already in an array:

expect(actual).to match_array(expected)
actual = ["editor", "admin", "viewer"]
expected = ["viewer", "editor", "admin"]

expect(actual).to match_array(expected)

The expectation passes because all three roles exist once in each array, even though their order differs.

Use contain_exactly when you want to write the expected elements directly:

expect(actual).to contain_exactly("viewer", "editor", "admin")

If your expected values are stored in an array, splat (*) it into separate arguments:

expected = ["viewer", "editor", "admin"]

expect(actual).to contain_exactly(*expected)

Duplicate-aware example

expect([1, 2, 2, 3]).to match_array([2, 3, 2, 1])

This passes. In contrast, the following fails because the actual array has only one 2:

expect([1, 2, 3]).to match_array([1, 2, 2, 3])

Matching objects that cannot be sorted

Task = Struct.new(:name)

first = Task.new("write tests")
second = Task.new("deploy")

expect([second, first]).to match_array([first, second])

No <=> method is needed because the matcher does not sort the arrays.

Step by Step Execution

Consider this expectation:

actual = ["pending", "paid", "pending"]
expected = ["paid", "pending", "pending"]

expect(actual).to match_array(expected)

What RSpec verifies conceptually:

  1. It does not compare index 0 with index 0, so "pending" and "paid" being in different first positions is not a problem.
  2. It finds one "paid" in actual and matches it with the one "paid" in expected.
  3. It finds two "pending" entries in each array.
  4. There are no elements left unmatched in either collection.
  5. The expectation passes.

Now change actual:

actual = ["pending", "paid"]
expected = ["paid", "pending", "pending"]

expect(actual).to match_array(expected)

RSpec can match "paid" and one "pending", but the second expected "pending" has no counterpart. The expectation fails. This is the important difference from to_set, which would treat both arrays as containing only "pending" and "paid".

Real World Use Cases

match_array is useful whenever a result is a collection but its ordering is not part of the contract.

  • Database queries: Verify returned records without relying on an unspecified database order.
  • API JSON: Check a response contains exactly the expected tags, permissions, or error codes.
  • Authorization: Confirm a user has the expected roles in any order.
  • Event processing: Verify a job emitted the expected events, including repeated events.
  • Parser output: Check a parser found the expected tokens when their collection order is irrelevant.
  • Service calls: Test the arguments gathered for a bulk operation without depending on insertion order.

If order is promised to users or callers—for example, newest-first notifications—test the order explicitly with eq.

Take Quiz

Real Codebase Usage

In production test suites, developers often use unordered collection matchers to avoid brittle tests. A test should fail when behavior changes, not merely because an implementation or database returned otherwise-valid items in a different order.

Testing an API field

body = JSON.parse(response.body)

expect(body.fetch("roles")).to match_array(["admin", "editor"])

Combining with nested matchers

match_array works well when each element is itself checked with an RSpec matcher:

expect(users).to match_array([
  have_attributes(email: "ana@example.com", active: true),
  have_attributes(email: "ben@example.com", active: false)
])

This verifies that the collection contains one user matching each description, in either order.

Prefer exactness when exactness is intended

match_array rejects extra items. If the requirement is only that some items are included, use include instead:

expect(roles).to include("admin")

That distinction makes tests communicate their requirements clearly: match_array means “exactly these items, any order.”

Common Mistakes

Using a set when duplicates matter

This test can pass even though the arrays have different counts:

# Avoid for duplicate-sensitive comparisons
expect(["error", "error"].to_set).to eq(["error"].to_set)

Use match_array instead:

expect(["error", "error"]).to match_array(["error"])

This correctly fails.

Sorting values that are not comparable

# May raise an error when objects do not implement <=>
expect(tasks.sort).to eq(expected_tasks.sort)

Use match_array to avoid requiring a sort order:

expect(tasks).to match_array(expected_tasks)

Forgetting the splat with contain_exactly

This is usually incorrect when expected is an array:

expected = ["admin", "editor"]
expect(roles).to contain_exactly(expected)

RSpec interprets that as expecting one element: the array itself. Expand the array into arguments:

expect(roles).to contain_exactly(*expected)

Or use match_array(expected).

Using include when no extra elements are allowed

expect(["admin", "editor", "unexpected"]).to include("admin", "editor")

This passes, because include permits extra values. Use match_array if the collection must contain no extras.

Ignoring order when order is required

Do not use match_array for ordered output such as priority lists:

expect(priorities).to eq(["critical", "high", "normal"])

Here, changing the order should fail the test.

Comparisons

ApproachIgnores order?Detects duplicate count differences?Requires elements to be sortable?Best use
expect(actual).to eq(expected)NoYesNoOrder is part of the contract
expect(actual).to match_array(expected)YesYesNoExactly the same items, any order
expect(actual).to contain_exactly(*items)YesYesNoSame as match_array, with inline expected items
expect(actual).to include(*items)YesNoNoRequired items may appear among extras
actual.to_set == expected.to_setYesNoNoOnly when duplicate counts truly do not matter
actual.sort == expected.sortYesYesYesRarely needed; only for naturally comparable values

match_array and contain_exactly are the usual RSpec choices for exact unordered comparisons. Choose match_array for an expected array and contain_exactly for a readable list of expected items.

Take Quiz

Cheat Sheet

# Exact elements, any order
expect(actual).to match_array(expected_array)

# Exact elements, any order, written inline
expect(actual).to contain_exactly(item_a, item_b)

# Expand an expected array for contain_exactly
expect(actual).to contain_exactly(*expected_array)

# Exact elements, order matters
expect(actual).to eq(expected_array)

# Includes values; extras are allowed
expect(actual).to include(item_a, item_b)

Rules:

  • match_array ignores position but checks all elements.
  • Duplicate values are counted.
  • Extra or missing values fail the expectation.
  • Elements do not need to implement <=>.
  • Use eq when sequence order is meaningful.
  • Legacy RSpec syntax is actual.should match_array(expected), but expect(actual).to ... is the preferred modern style.

FAQ

How do I compare two arrays without considering order in RSpec?

Use:

expect(actual).to match_array(expected)

It requires both arrays to contain exactly the same elements, regardless of position.

Does match_array handle duplicate values?

Yes. It checks multiplicity. [1, 1] does not match [1].

What is the difference between match_array and contain_exactly?

They express the same exact unordered comparison. match_array accepts one expected array; contain_exactly accepts expected elements as separate arguments.

expect(actual).to match_array([1, 2])
expect(actual).to contain_exactly(1, 2)

Do I need to sort arrays before using match_array?

No. match_array is designed for unordered comparisons and does not require sorting.

When should I use include instead of match_array?

Use include when the actual collection may contain additional values. Use match_array when extra values should cause a failure.

Can match_array test arrays of objects?

Yes. It can compare objects directly or combine with matchers such as have_attributes when you want to check selected object properties.

Why is comparing to_set not enough?

Sets discard duplicates. If duplicate count is meaningful, set comparison can incorrectly pass.

Take Quiz

Mini Project

Description

Create a small RSpec example for a permission service. The service may return permissions in any order, but the test must ensure it returns exactly the permissions expected. This reflects common API and authorization testing, where ordering is often not guaranteed.

Goal

Write an order-independent test that still detects missing, extra, and duplicate permissions.

Requirements

  1. Create a PermissionService class with a permissions_for method.
  2. Return three permissions for an administrator in a deliberately non-alphabetical order.
  3. Write an RSpec test using match_array.
  4. Verify the test expects exactly the three permitted actions.
  5. Do not sort the returned or expected arrays.
Take Quiz

Keep learning

Related questions

View more