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
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.
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