Search code examples
ruby-on-railsrubyrspecdelegationruby-on-rails-5.2

rspec testing delegation for module method undefined method


This module gets included in a form object within rails.

What is the right way to test it using rspec?

1) Do I test it directly on each model that includes it?

or

2) Do I test the delegation method directly? (i would prefer direct if possible)

If I test it directly, how? I tried and get the below error...

Form Object Module

module Registration
  class Base
    module ActAsDelegation
      def self.included(base)
        base.extend(ClassMethods)
      end

      module ClassMethods
        def form_fields_mapping
          [ 
            {name: :first, model: :user},
            {name: :address, model: :address}
          ]
        end

        def fields_of_model(model)
          form_fields_mapping.select {|record| record[:model] == model }.map {|record| record[:name] }
        end

        def delegate_fields_to(*models)
          models.each do |model|
            fields_of_model(model).each do |attr|
              delegate attr.to_sym, "#{attr}=".to_sym, to: model if attr.present?
            end
          end
        end
      end
    end
  end
end

Form Object

module Registration
  class Base
    include ActiveModel::Model
    include ActAsDelegation

    def initialize(user=nil, attributes={})
      error_msg = "Can only initiate inherited Classes of Base, not Base Directly"
      raise ArgumentError, error_msg if self.class == Registration::Base

      @user = user
      setup_custom_accessors

      unless attributes.nil?
        (self.class.model_fields & attributes.keys.map(&:to_sym)).each do |field|
          public_send("#{field}=".to_sym, attributes[field])
        end
      end
      validate!
    end
  end
end

RSPEC TESTING

require "rails_helper"

RSpec.describe Registration::Base::ActAsDelegation, type: :model do

  describe "Class Methods" do
    context "#delegate_fields_to" do
      let(:user) {spy('user')}
      let(:address) {spy('address')}
      let(:delegation_fields) {          [ 
            {name: :first, model: :user},
            {name: :address, model: :address}
          ]}

      it "should delegate" do
        allow(subject).to receive(:form_fields_mapping) { delegation_fields }
        Registration::Base::ActAsDelegation.delegate_fields_to(:user,:address)
        expect(user).to have_received(:first)
        expect(address).to have_received(:address)
      end
    end
  end
end

ERROR

Failure/Error: Registration::Base::ActAsDelegation.delegate_fields_to(:user,:address)

 NoMethodError:
   undefined method `delegate_fields_to' for Registration::Base::ActAsDelegation:Module
   Did you mean?  delegate_missing_to

(I have other code issues in this example, but below resoved the main issue)


Solution

  • As your module is designed to be included, just include it in an empty class in tests. I prepared a simplified example which I verified to work:

    module ToBeIncluded
      def self.included(base)
        base.extend(ClassMethods)
      end
    
      module ClassMethods
        def a_class_method
          :class
        end
      end
    end
    
    class TestSubject
      include ToBeIncluded
    end
    
    require 'rspec/core'
    
    RSpec.describe ToBeIncluded do
      subject { TestSubject }
    
      it 'returns correct symbol' do
        expect(subject.a_class_method).to eq(:class)
      end
    end
    

    In your case probably something along those lines should be fine:

    require "rails_helper"
    
    RSpec.describe Registration::Base::ActAsDelegation, type: :model do
      class TestClass
        include Registration::Base::ActAsDelegation
      end
    
      describe "Class Methods" do
        context "#delegate_fields_to" do
          let(:user) {spy('user')}
          let(:address) {spy('address')}
          let(:delegation_fields) {          [ 
                {name: :first, model: :user},
                {name: :address, model: :address}
              ]}
    
          it "should delegate" do
            allow(TestClass).to receive(:form_fields_mapping) { delegation_fields }
            TestClass.delegate_fields_to(:user,:address)
            expect(user).to have_received(:first)
            expect(address).to have_received(:address)
          end
        end
      end
    end
    

    Also, you could make anonymous class if you are afraid of name clashes.