Testing Ruby decorators with super_method

Simone Bravo

8 Apr 2020 Development, Ruby On Rails, Testing

Simone Bravo

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

You may also like

Let’s redefine
eCommerce together.