RSpec Matchers Deep Dive

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:

  1. Primitive Matchers: These deal with basic data types like numbers, strings, and booleans. They perform simple, direct checks.
  2. Higher-Order Matchers: These are more advanced and can accept other matchers as input. They help you build complex assertions.
  3. 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 as 90.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 if student2 points to the same object in memory as student1.
  • 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) or eq(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 and end_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


Posted

in

by