
August 3, 2026
If you’ve been writing Ruby tests for a while, chances are you’ve reached for eq more times than you can count. While it works for many scenarios, RSpec Expectations offers a much richer vocabulary for expressing intent.
Choosing the right matcher isn’t just about making a test pass—it’s about communicating exactly what behavior your code should exhibit.
Let’s explore some of the most useful matcher categories and when each one should be used.
Equality Isn’t Just Equality
One of the first surprises for many developers is that RSpec provides multiple ways to compare objects.
expect(actual).to eq(expected)expect(actual).to eql(expected)expect(actual).to be(expected)expect(actual).to equal(expected)
Although these look similar, they’re designed for different purposes.
- eq succeeds when actual == expected.
- eql relies on Ruby’s eql? method.
- be and equal verify object identity using equal?.
The distinction becomes important when comparing value objects versus checking that two variables reference the exact same object.
Selecting the appropriate matcher makes your expectations more explicit and easier to understand.
Truthiness vs. Boolean Values
Ruby treats every object except nil and false as truthy.
RSpec reflects this distinction with dedicated matchers.
expect(user).to be_truthyexpect(result).to be trueexpect(error).to be_falsyexpect(flag).to be falseexpect(record).to be_nil
These matchers let you communicate whether you’re expecting a strict Boolean value or simply a truthy or falsy result.
Working with Collections
RSpec includes specialized matchers for collections, allowing tests to describe exactly what matters.
Exact Order
expect(numbers).to eq([1, 2, 3])
Partial Matches
expect(numbers).to start_with(1)expect(numbers).to end_with(3)expect(numbers).to include(2)
Ignore Ordering
expect(numbers).to contain_exactly(3, 2, 1)expect(numbers).to match_array([3, 2, 1])
Instead of manually sorting arrays or writing verbose assertions, these matchers describe your intent directly.
Expectations on Blocks
Some behavior isn’t about return values.
Sometimes what matters is what happens while executing a block.
RSpec provides dedicated matchers for these situations.
Errors
expect { service.call }.to raise_errorexpect { service.call }.to raise_error(MyError)expect { service.call }.to raise_error("Something went wrong")
Yielding
For methods that accept blocks, Expectations includes several yield matchers.
expect { |b| 5.tap(&b) }.to yield_controlexpect { |b| 5.tap(&b) }.to yield_with_args(5)expect { |b| [1,2,3].each(&b) } .to yield_successive_args(1,2,3)
These allow tests to verify block behavior without relying on implementation details.
Composing Expectations
One of the more expressive capabilities in RSpec Expectations is combining matchers.
expect(alphabet) .to start_with("a") .and end_with("z")
You can also compose matchers inside other matchers.
expect(hash).to match( a: { b: a_collection_containing_exactly( a_string_starting_with("f"), an_instance_of(Integer) ) })
Rather than comparing entire complex objects, you can specify only the characteristics that matter.
This results in tests that are both more flexible and easier to read.
Choosing the Right Matcher
A good expectation answers a very specific question.
- Are you comparing values?
- Are you checking object identity?
- Does order matter?
- Are you validating part of a collection?
- Should an exception be raised?
- Does a method yield to a block?
RSpec includes dedicated matchers for each of these scenarios. Choosing the one that best matches your intent makes your tests more expressive and easier to maintain.
Final Thoughts
RSpec Expectations goes far beyond eq. It provides a comprehensive set of matchers for values, identity, collections, exceptions, yielding behavior, and composable assertions.
The next time you write an expectation, take a moment to ask whether eq is really expressing what you mean. More often than not, there’s a matcher that communicates your intent more precisely—and that’s exactly what good tests should do.
