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