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 asexpected, 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_arraypours 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.
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:
- It does not compare index
0with index0, so"pending"and"paid"being in different first positions is not a problem. - It finds one
"paid"inactualand matches it with the one"paid"inexpected. - It finds two
"pending"entries in each array. - There are no elements left unmatched in either collection.
- 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.
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
| Approach | Ignores order? | Detects duplicate count differences? | Requires elements to be sortable? | Best use |
|---|---|---|---|---|
expect(actual).to eq(expected) | No | Yes | No | Order is part of the contract |
expect(actual).to match_array(expected) | Yes | Yes | No | Exactly the same items, any order |
expect(actual).to contain_exactly(*items) | Yes | Yes | No | Same as match_array, with inline expected items |
expect(actual).to include(*items) | Yes | No | No | Required items may appear among extras |
actual.to_set == expected.to_set | Yes | No | No | Only when duplicate counts truly do not matter |
actual.sort == expected.sort | Yes | Yes | Yes | Rarely 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.
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_arrayignores position but checks all elements.- Duplicate values are counted.
- Extra or missing values fail the expectation.
- Elements do not need to implement
<=>. - Use
eqwhen sequence order is meaningful. - Legacy RSpec syntax is
actual.should match_array(expected), butexpect(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.
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
- Create a
PermissionServiceclass with apermissions_formethod. - Return three permissions for an administrator in a deliberately non-alphabetical order.
- Write an RSpec test using
match_array. - Verify the test expects exactly the three permitted actions.
- Do not sort the returned or expected arrays.
Keep learning
Related questions
Add a Default Value to an Existing Column in a Rails Migration
Learn how to add or change a default value on an existing database column using Rails Active Record migrations safely.
Calling a Class Method from an Instance in Ruby
Learn how to call a class method from an instance in Ruby using self.class, with examples, pitfalls, and practical usage patterns.
Calling an Overridden Monkey-Patched Method in Ruby
Learn how to call the original method when monkey patching in Ruby, including alias_method patterns, examples, pitfalls, and practical usage.