Home > Back-end >  RSpec says 'undefined method' when running against module that includes another module
RSpec says 'undefined method' when running against module that includes another module

Time:06-15

my situation is the current:

module Base
 module A
  include Base::B
 end
end

module Base
 module B
  module_function
  def my_method
   ...
  end
 end
end

And then my test:

Rspec.describe Base::A do
 describe '.my_method' do
  it 'does something'
   ...
  end
 end
end

Problem:

NoMethodError. undefined method `my_method' for Base::A:Module

What am I missing? Thanks in advance!

CodePudding user response:

module Base
  module B
    module_function

    def my_method
      "you've found me!"
    end
  end
end

module Base
  module A
    include Base::B
  end
end

module_function lets us call our method on the module by defining a singleton method:

>> Base::B.singleton_methods
=> [:my_method]
>> Base::B.my_method
=> "you've found me!"

it also turns my_method into a private method, this is the method you get when "including" or "extending":

>> Base::B.private_instance_methods
=> [:my_method]

# NOTE: we can call this version of `my_method` by including or 
#       extending the module in a class

class Included
  include Base::B
end

>> Included.private_instance_methods
=> [:my_method, ...]
>> Included.new.send(:my_method)
=> "you've found me!"

class Extended
  extend Base::B
end

# this would be a private class method
>> Extended.private_methods
=>[:my_method, ...]
>> Extended.send(:my_method)
=> "you've found me!"

Same rules apply if Base::B is included in Base::A module:

>> Base::A.private_instance_methods
=> [:my_method]

# NOTE: we cannot call `my_method` on the module, because we do not get a 
#       singleton method
>> Base::A.singleton_methods
=> []

# there is the expected instance method, made private by using `module_function`
>> Base::A.private_instance_methods
=> [:my_method]

# only we can't call `.new` on a module.
>> Base::A.new.send(:my_method)
`<main>': undefined method `new' ...

Since we can't initialize an instance out of the module, we can bump the instance method to a class method by extending Base::A with self.

module Base
  module A
    include Base::B
    extend self
  end
end

>> Base::A.private_methods
=> [:my_method, ...]
>> Base::A.send(:my_method)
=> "you've found me!"

or extend Base::B from the start:

module Base
  module A
    extend Base::B
  end
end

>> Base::A.send(:my_method)
=> "you've found me!"

module_function does the equivalent of this:

module Base
  module C
    # NOTE: singleton method stays in this module
    def self.my_method
      "singleton my_method"
    end
    
    private

    # NOTE: instance method gets included/extended
    def my_method
      "private instance my_method"
    end
  end
end

Base::C.my_method                               # => "singleton my_method"
Module.new.extend(Base::C).send(:my_method)     # => "private instance my_method"
Class.new.extend(Base::C).send(:my_method)      # => "private instance my_method"
Class.new.include(Base::C).new.send(:my_method) # => "private instance my_method"

https://ruby-doc.org/core-3.1.2/Module.html#method-i-module_function

https://ruby-doc.org/core-3.1.2/Module.html#method-i-include

https://ruby-doc.org/core-3.1.2/Object.html#method-i-extend

  • Related