
Why Do We Need Expectations?
Imagine you’re building a Learning Management System (LMS). You might have a class that handles student enrollments. How do you know if your enrollment code is working correctly? You could manually test it, but that’s time-consuming and prone to errors. That’s where rspec-expectations
comes in. It allows you to write automated tests that check if your code behaves as you intend.
rspec-core
provides the structure for your tests, but rspec-expectations
is what allows you to actually verify the results.
The Core Idea: Expectations
At its heart, an expectation is a statement about what you believe should be true at a specific point in your code. If that statement is not true, RSpec will tell you that your test has failed, along with a helpful message.
Anatomy of an Expectation
An expectation has three main parts:
- Subject: This is the thing you’re testing. It could be a variable, an object, or the result of a method call.
- Matcher: This is an object that defines what you expect to be true about the subject. It’s like a rule that the subject must follow.
- Custom Failure Message (Optional): This is a message that provides extra context when an expectation fails. It helps you understand why the test failed.
These parts are connected using expect
, along with either to
or not_to
.
Example:
# Let's say we have a class for managing courses in our LMS
class Course
attr_reader :students
def initialize
@students = []
end
def enroll(student)
@students << student
end
end
# Here's how we might test it using RSpec
RSpec.describe Course do
it 'should enroll a student' do
course = Course.new
student = "Alice"
course.enroll(student)
expect(course.students).to include(student), 'Student was not enrolled'
end
end
In this example:
course.students
is the subject.include(student)
is the matcher. It checks if the students array includes the student variable.'Student was not enrolled'
is the custom failure message.
Diving Deeper: expect, to, and not_to
expect
The expect
method is like a wrapper that prepares your subject for testing. It takes the subject as an argument and returns an object that you can then use with to
or not_to
.
expect_example = expect(5)
# => #<RSpec::Expectations::ExpectationTarget:0x007fb4eb83a818 @target=5>
Matchers
Matchers are the heart of expectations. They perform the actual test by checking if the subject meets the specified criteria. RSpec provides many built-in matchers for common checks, like:
eq(value)
: Checks if the subject is equal to the given value.be_within(delta).of(value)
: Checks if the subject is within a certain range of the given value.include(value)
: Checks if the subject (an array or string) includes the given value.be_empty
: Checks if the subject (an array or string) is empty.be_true
: Checks if the subject is true.be_false
: Checks if the subject is false.be_nil
: Checks if the subject is nil.
be_five = eq(5)
# => #<RSpec::Matchers::BuiltIn::Eq:0x007fb4eb82dd98 @expected=5>
to and not_to
to
: This method checks if the subject matches the matcher. If it does, the test passes. If not, the test fails.not_to
: This method checks if the subject does not match the matcher. If it doesn’t, the test passes. If it does, the test fails.to_not
: This is an alias for not_to.
expect(5).to eq(5) # => true (test passes)
expect(5).not_to eq(6) # => true (test passes)
expect(5).to_not eq(6) # => true (test passes)
expect(5).not_to eq(5) # Raises RSpec::Expectations::ExpectationNotMetError (test fails)
When Expectations Fail
When an expectation fails, RSpec provides a detailed error message. This message tells you:
- Which expectation failed.
- What the subject was.
- What the matcher was expecting.
- (If provided) Your custom failure message.
Example:
resp = Struct.new(:status, :body).new(404, 'Not Found')
expect(resp.status).to eq(200), "Expected status 200, but got #{resp.status}: #{resp.body}"
# Raises RSpec::Expectations::ExpectationNotMetError with the custom message
RSpec Expectations vs. Traditional Assertions

You might be familiar with traditional assertions (like assert in other testing frameworks). RSpec expectations are similar but offer several advantages:
- Composability: Matchers can be combined in flexible ways.
- Negation: Matchers can be automatically negated using
not_to
. - Readability: The syntax reads like an English description of the expected outcome.
- More Useful Errors: Provides detailed information about failures.
Example:
expect([1, 2, 3, 4]).to all be_odd
# Error message:
# expected [1, 2, 3, 4] to all be odd
# object at index 1 failed to match:
# expected `2.odd?` to return true, got false
How Matchers Work (Simplified)
Matchers are objects that have two main methods:
matches?(actual)
: Returns true if the subject matches, false otherwise.failure_message
: Returns a message to display when the match fails.
Composing Matchers
Matchers can be combined in several ways to create more complex expectations:
- Passing one matcher into another:
grades = [85, 92, 78, 95]
expect(grades).to start_with( be_within(5).of(90) )
# Checks if the first grade is within 5 of 90
- Embedding matchers in arrays and hashes:
student = { name: 'Bob', grade: 88 }
expect(student).to include(name: 'Bob', grade: a_value_between(80, 90))
- Combining matchers with logical and/or operators:
expect(10).to be_even.and be > 5
expect(7).to be_even.or be > 5
Generated Example Descriptions
RSpec can generate descriptions for your tests based on the code. This can reduce the amount of code you need to write.
RSpec.describe Course, '#enroll' do
subject { Course.new }
it { is_expected.to respond_to(:enroll) }
it { should_not respond_to(:drop) }
end
subject
: Defines the object being tested.is_expected
: Shorthand forexpect(subject)
.should
: Local alias forexpect(subject).to
.