Skip to main content

delegate_to

What is delegate_to?

delegate_to is a custom RSpec matcher that provides a neat way to check whether a delegation was performed as expected.

For example, consider the following code snippet:

class FirstService
attr_reader :params

def initialize(params:)
@params = params
end

def result
# ...
SecondService.result(params: params)
end
end

As you can see, the first service asks the second one to do some "interesting" stuff and returns its value.

This behavior can be easily tested in RSpec like so:

RSpec.describe FirstService do
describe ".result" do
let(:params) { {foo: :bar} }

it "delegates to `SecondService.result`" do
# Creates a spy.
allow(SecondService).to receive(:result).and_call_orginal

FirstService.result(params: params)

# Confirms delegation.
expect(SecondService).to have_received(:result)
end

it "returns `SecondService.result`" do
# Confirms return value equality.
expect(FirstService.result(params: params)).to eq(SecondService.result(params: params))
end
end
end

Since it is so common to write specs as above when working with services, the delegate_to matcher is provided to hide the repeatable noise.

RSpec.describe FirstService do
describe ".result" do
include ConvenientService::RSpec::Matchers::DelegateTo

let(:params) { {foo: :bar} }

it "delegates to `SecondService.result`" do
expect { FirstService.result(params: params) }.to delegate_to(SecondService, :result)
end

it "returns `SecondService.result`" do
# Confirms return value equality.
expect(FirstService.result(params: params)).to eq(SecondService.result(params: params))
end
end
end

delegate_to has a self-explanatory and_return_its_value chaining, so we can refactor out the second spec as well.

RSpec.describe FirstService do
describe ".result" do
include ConvenientService::RSpec::Matchers::DelegateTo

let(:kwargs) { {foo: :bar} }

it "delegates to and returns `SecondService.result`" do
expect { FirstService.result(params: params) }
.to delegate_to(SecondService, :result)
.and_return_its_value
end
end
end
danger

and_return_its_value uses RSpec eq to compare values, therefore ensure your return values define meaningful == operator.

Also delegate_to supports with_arguments chaining to make sure that delegation happened with certain arguments.

RSpec.describe FirstService do
describe ".result" do
include ConvenientService::RSpec::Matchers::DelegateTo

let(:params) { {foo: :bar} }

it "delegates to and returns `SecondService.result`" do
expect { FirstService.result(params: params) }
.to delegate_to(SecondService, :result)
.with_arguments(params: params)
.and_return_its_value
end
end
end

This spec can be simplified even further.

RSpec.describe FirstService do
describe ".result" do
include ConvenientService::RSpec::Matchers::DelegateTo

let(:params) { {foo: :bar} }

specify do
expect { FirstService.result(params: params) }
.to delegate_to(SecondService, :result)
.with_arguments(params: params)
.and_return_its_value
end
end
end

When specify is utilized, RSpec infers spec description from delegate_to arguments and chainings.

delegate_to is context-independent

In other words, delegate_to is applicable for any classes, not only for services, e.g:

class Notifier
# ...
def broadcast_event(*args, **kwargs, &block)
Event.create(*args, **kwargs, &block)
end
end

With specs:

RSpec.describe Notifier do
describe "#broadcast_event" do
include ConvenientService::RSpec::Matchers::DelegateTo

let(:notifier) { create(:notifier) }

let(:args) { :foo }
let(:kwargs) { {foo: :bar} }
let(:block) { proc { :foo } }

specify do
expect { notifier.broadcast_event(*args, **kwargs, &block) }
.to delegate_to(Event, :create)
.with_arguments(*args, **kwargs, &block)
.and_return_its_value
end
end
end

Please, note that delegate_to is able to verify block argument (&block) that is not possible with raw allow to receive.

danger

Blocks (procs, lambdas) are compared by RSpec eq under the hood.

Please, remember their semantics in order to not be overly surprised.

The two Proc instances may look the same from the syntax point of view.

But they are different from the Proc semantics point of view.

first = proc { :foo }
second = proc { :foo }

first == second # => false
first == first # => true
second == second # => true