
What are Matchers?
In RSpec, you use expect blocks to make assertions about your code. Matchers are the part of the expect block that specifies what you’re expecting. They’re like the verbs in your testing sentences.
Categories of Matchers
We can broadly categorize matchers into three groups:
- Primitive Matchers: These deal with basic data types like numbers, strings, and booleans. They perform simple, direct checks.
- Higher-Order Matchers: These are more advanced and can accept other matchers as input. They help you build complex assertions.
- Block Matchers: These are used to test the behavior of code blocks, like whether they raise errors or yield values.
Let’s dive into each category!
1. Primitive Matchers: The Basics
Primitive matchers are your go-to tools for simple checks. They’re like the basic building blocks of your tests.
Equality and Identity
These matchers help you answer the question: “Are these two things the same?” But “same” can mean different things in programming.
- Value Equality (eq): This is the most common type of equality. It checks if two objects have the same value. It’s like comparing two apples to see if they’re both red and the same size.
# Example in a Learning Management System (LMS) context:
# Let's say we have a method to calculate the average grade of a student
def calculate_average_grade(grades)
grades.sum.to_f / grades.size
end
# In our test:
grades = [80, 90, 100]
average = calculate_average_grade(grades)
expect(average).to eq(90.0) # Checks if the calculated average is 90.0
- Here,
eq(90.0)
checks if the average variable has the same value as90.0.
- Identity (equal or be): This checks if two variables refer to the exact same object in memory. It’s like checking if two people are the same person, not just two people with the same name.
# Example in LMS context:
# Let's say we have a class for a student
class Student
attr_accessor :name
def initialize(name)
@name = name
end
end
student1 = Student.new("Alice")
student2 = student1 # student2 now refers to the same object as student1
expect(student2).to equal(student1) # Checks if student1 and student2 are the same object
expect(student2).to be(student1) # Same as above, just a different way to write it
- Here,
equal(student1)
checks ifstudent2
points to the same object in memory asstudent1
. - Hash Key Equality (eql): This is similar to
eq
, but it considers integers and floats as different, even if they have the same numerical value. This is important when using them as keys in a hash.
expect(3).to eq(3.0) # This is true because they have the same value
expect(3).not_to eql(3.0) # This is true because they are different types
- In most cases, you’ll want to use eq.
Truthiness
In Ruby, false
and nil
are considered “falsey,” and everything else is “truthy.”
be_truthy
: Checks if a value is considered true.be_falsey
: Checks if a value is considered false.
expect(true).to be_truthy
expect(1).to be_truthy
expect(false).to be_falsey
expect(nil).to be_falsey
- For precise true or false checks, use
eq(true)
oreq(false)
.
Operator Comparisons
You can use be with comparison operators like <
, >
, ==
, ===
, and =~
.
expect(5).to be > 3
expect(5).to be == 5
expect("hello").to be =~ /ell/
Delta and Range Comparisons
- Absolute Difference (be_within): Useful for floating-point numbers, which can have slight inaccuracies. It checks if a value is within a certain range of an expected value.
expect(0.1 + 0.2).to be_within(0.0001).of(0.3)
- Relative Difference (percent_of): Checks if a value is within a certain percentage of another value.
expect(105).to be_within(5).percent_of(100)
- Ranges (be_between): Checks if a value falls within a specified range.
expect(7).to be_between(5, 10)
Dynamic Predicates
Ruby methods that end with a ?
(like empty
? or odd?
) are called predicate methods. RSpec lets you use them directly with the be_
prefix.
expect([]).to be_empty # Calls [].empty?
expect(5).to be_odd # Calls 5.odd?
Satisfaction
The satisfy matcher lets you use a block to define complex conditions.
expect(7).to satisfy { |number| number.odd? && number > 5 }
2. Higher-Order Matchers: Building Complex Assertions
Higher-order matchers take other matchers as input, allowing you to create more complex and specific tests.
Collections and Strings
include
: Checks if a collection or string contains certain items.
expect([1, 2, 3]).to include(2)
expect("hello").to include("ell")
start_with
andend_with
: Checks if a collection or string starts or ends with specific items.
expect([1, 2, 3]).to start_with(1)
expect("hello").to end_with("lo")
all
: Checks if all items in a collection satisfy a given matcher.
expect([2, 4, 6]).to all be_even
match
: Checks a data structure against a pattern, using matchers for elements or values.
expect([1, "hello", 3]).to match([a_value > 0, a_string_including("ell"), a_value < 5])
contain_exactly
: Checks if a collection contains exactly the specified items, regardless of order.
expect([1, 2, 3]).to contain_exactly(3, 1, 2)
Object Attributes
have_attributes
: Checks an object’s attributes against a template.
class Course
attr_accessor :name, :credits
def initialize(name, credits)
@name = name
@credits = credits
end
end
course = Course.new("Math", 3)
expect(course).to have_attributes(name: "Math", credits: 3)
3. Block Matchers: Testing Code Blocks
Block matchers are used to test the behavior of code blocks.
Raising and Throwing
raise_error
: Checks if a block raises an exception
expect { raise "Something went wrong" }.to raise_error("Something went wrong")
throw_symbol
: Checks if a block throws a symbol.
expect { throw :my_symbol }.to throw_symbol(:my_symbol)
Yielding
yield_control
: Checks if a method yields to a block
def my_method
yield
end
expect { |block| my_method(&block) }.to yield_control
yield_with_args
: Checks the arguments yielded by a method.
def my_method
yield(1, "hello")
end
expect { |block| my_method(&block) }.to yield_with_args(1, "hello")
Mutation
change
: Checks if a value changes after running a block of code.
array = [1, 2, 3]
expect { array << 4 }.to change { array.size }.by(1)
Output
output
: Checks output to stdout or stderr.
expect { print "Hello" }.to output("Hello").to_stdout