Testing Ruby decorators with super_method

Often we need to override a simple method which returns a data set, and most of the times you just need to add a couple of fields. In this blog post, I’m going to suggest a simple way to test this scenario without writing complex specs.
Here’s a simple code example to better understand what I’m talking about:
class Address
def info
{
first_name: 'Simone',
last_name: 'Bravo',
address: 'via Accademia 10, Monopoli'
}
end
end
module AddPhoneNumberToInfo
def info
super.merge(phone_number: '123456789')
end
Address.prepend self
end
Before discovering the super_method
(we will talk later about that) I used to
test these situations in two different ways:
- Write tons of lines of code to exactly match the data set returned by the overridden method.
RSpec.describe Address do
describe '#info' do
let(:expected_hash) do
{
first_name: 'Simone',
last_name: 'Bravo',
address: 'via Accademia 10, Monopoli',
phone_number: '123456789'
}
end
subject { described_class.new.info }
it 'adds the phone number to the default address' do
expect(subject).to eq expected_hash
end
end
end
- Test part of the data set without ensuring that the original data is preserved
RSpec.describe Address do
describe '#info' do
subject { described_class.new.info }
it 'adds the phone number to the default address' do
expect(subject).to include(phone_number: '123456789')
end
end
end
Luckily there is a solution to this madness:
The super super_method method
super_method
shows us how beautiful ruby is, allowing us to navigate up in the
ruby stack and call parent methods. Let’s say we have a class extending a
superclass
class ParentClass
def a_method
'This method is contained in the parent class'
end
end
class ChildClass < ParentClass
def a_method
'This method is contained in the child class'
end
end
We all know that calling ChildClass.new.a_method
will return
'This method is contained in the child class'
, but what if we want to access
the ParentClass
?
Using super_method
we can jump up one class in our ChildClass
stack, we just
need to call ChildClass.new.method(:a_method).super_method.call
to get the
string 'This method is contained in the parent class'
.
Back to our specs
Now we have a way to call the original method we overrode, so we can just test
that calling #info
returns both original data and the new phone_number
field. Here’s an example:
RSpec.describe Address do
describe '#info' do
subject { described_class.new.info }
it 'adds the phone number to the default address' do
expect(subject).to include(phone_number: '123456789')
expect(subject).to include(
described_class.new.method(:info).super_method.call
)
end
end
end
This is cool, but you know what is way cooler? An RSpec custom matcher! Wouldn’t
be nice to just call
expect(subject).to add_to_super_class_method(:info, phone_number: '123456789')
? Let’s see how we can add this cool feature to our specs:
RSpec.configure do |config|
RSpec::Matchers.define :add_to_super_class do |method, new_fields|
old_fields = current_hash.method(method).super_method.call
match do |current_hash|
expect(current_hash).to include(new_fields)
expect(current_hash).to include(old_fields)
end
failure_message do |current_hash|
"Expected #{new_fields} to be added to #{old_fields}\n"\
"Current result: #{current_hash}"
end
end
end
If you’re thinking to further explore custom matchers, here’s the official relish documentation page.
A little extra
If you, like me, prefer to avoid using class evals to override methods, check out this new gem we recently created to achieve this result in a cleaner way: nebulab/prependers
That’s all for this blog post, I hope you enjoyed the reading and it will help you with your specs.