Skip to Main Content

TWIL 2022-09-23

Katie demonstrating RSpec matchers in Ruby and Rails during the "This Week I Learned" series to improve testing techniques.

Welcome to TWIL, our week-in-review of micro learnings that effortlessly steamroll the complexities of software development into digestible insights. This week, Katie reveals the power of RSpec: Expect to receive with matchers in the Ruby and Rails ecosystem. Discover how RSpec's matchers refine tests, offering precise control over method calls and argument expectations, while ensuring your tests remain robust and intuitive.

RSpec: Expect to Receive with Matchers

With RSpec, you can stub a method call in your tests to prevent the actual method being called and optionally dictate the method call’s return value:

allow(Thing).to receive(:some_method)
allow(something).to receive(:some_other_method).and_return("whatever")
# Thing.some_method or something.some_other_method will no longer call
# the actual methods; additionally, something.some_other_method will 
# return the specified value ("whatever" in this example).

Stubbing a method like this also allows you to test that the method was called by other code:

# Code that should trigger the method calls, in this case
# Thing.some_method("some", "arguments") and something.some_other_method(*)
expect(Thing).to have_received(:some_method).with("some", "arguments")
expect(something).to have_received(:some_other_method)

This is very handy, but what if you want to ensure that your method is called with, for example, a specific ActiveRecord result?

# You will likely already have the objects in question set up already:
let(:thing_1) { create(:thing) }
let(:thing_2) { create(:thing) }

# Actual argument: #<ActiveRecord::Relation [#<Thing id: 1>, #<Thing id: 2>]>
array_of_things = [thing_1, thing_2]

# Code here that should call Thing.some_method with ActiveRecord relation

expect(Thing).to have_received(:some_method).with(array_of_things)
# ☝️ This will fail, because the spec as written expects the method to be
# called with an array but receives an ActiveRecord relation instead

It can be challenging to set up tests for method calls that expect more complex arguments as above, except… RSpec will accept matchers in place of explicit arguments!

# You will likely already have the objects in question set up already:
let(:thing_1) { create(:thing) }
let(:thing_2) { create(:thing) }

# Actual argument: #<ActiveRecord::Relation [#<Thing id: 1>, #<Thing id: 2>]>
array_of_things = [thing_1, thing_2]

# Code here that should call Thing.some_method with ActiveRecord relation

expect(Thing).to have_received(:some_method).with(
  contain_exactly(thing_1, thing_2),
)
# This amounts to a check for whether whatever Thing.some_method was, in fact,
# called with does contain_exactly(array_of_things)

This should work for any RSpec matcher, and there are even aliases (from RSpec 3) that can be used to improve readability, e.g., a_collection_containing_exactly instead of contain_exactly:

expect(Thing).to have_received(:some_method).with(
  a_collection_containing_exactly(array_of_things),
)

Resources

  • Ruby
  • Rails
  • Tests
Katie Linero's profile picture
Katie Linero

Senior Software Engineer

Related Posts