1

I was having trouble understanding an error while unit testing my module, which is a mixin.

Suppose the mixin to be tested is module A:

require 'path/b'    
module A
    def methodA()
        puts methodB.attr1 
    end
end

And it depends on another mixin B which was defined in a file at path/b.rb

module B
    def methodB
       return someObject #that has property 'attr1'
    end       
end

Now, we have a class to unit test module A

require 'path/moduleA'
class TestA
    include Path::moduleA
end

describe 'test moduleA.methodA'
  it 'does something'
     testObject = TestA.new
     testObject.methodA()
     expect(....)
  end
end  

I get following error on running the rspec test

NameError:
   undefined local variable or method `methodB' for #<TestA:0x00007f77a03db9a8>

I am able to resolve it by one of following ways:

  1. including module B in module A
  2. including module B in class TestA

Questions

  1. I am not clear why include is required to get access to methodB in module A and class TestA when 'require' was already added in module A.

  2. My intention is to use methods of module B in module A, but not let users of module A access module B methods automatically.

    • resolution 1 above gives users of A, access to B's methods

    • resolution 2 forces users of A (user -> the unit test class in this example) to include A's dependency B directly, even though user is only interested in accessing A's methods.

Hence, both resolutions don't achieve what I want. Is there a way to achieve it?

I'm new to Ruby so may be it doesn't support this. I'm from Java background where I would model A and B as two classes, make an instance of B as field of A, then expose A's own public methods to users of A. But since they are mixins, I need to use modules in ruby.

phalanx
  • 43
  • 9
  • I think my question is similar to https://stackoverflow.com/questions/322470/can-i-invoke-an-instance-method-on-a-ruby-module-without-including-it – phalanx Oct 15 '18 at 20:09

3 Answers3

4

Just to be very explicit: require / require_relative / load and include / extend / prepend have absolutely nothing whatsoever to do with each other.

The former three simply run a Ruby file. That's it. That is all they do. They differ in how and where they search for the file, but after they found the file, they don't do anything else than just execute it.

The latter three add a module to the ancestor chain. include essentially makes the module the superclass, extend is really the same as singleton_class.include (i.e. makes the module the superclass of the singleton class), and prepend inserts the module at the beginning of the ancestor's chain, i.e. actually before the class that it is prepended to.

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • If i include module B in module A, all of the methods of B become methods of A due to inheritance. However, i want to use methods of B in A without exposing them to users of A. Is it acheivable? Reason for this is that module A is a different class from module B and doesn't make sense to inherit it. One way is to let users of A include B but then they become aware of what modules A depends on, thats not right either. – phalanx Oct 15 '18 at 06:53
  • "If i include module B in module A, all of the methods of B become methods of A due to inheritance." – No, they don't. The methods of `B` are methods of `B`, period. If you include module `B` into module `A`, then the only thing that happens is that Ruby records the fact that you did this. Then, later, when you include `B` into some class `C`, Ruby will make `B` the new superclass of `C`, `A` the superclass of `B`, and the old superclass of `C` the superclass of `A`. At no point whatsoever does a method of one module or class become a method of another module or class. – Jörg W Mittag Oct 15 '18 at 21:55
0

require just tells ruby to read / load the code inside the ruby file. In this case it will just define the module. However in order for code inside a module to be included inside another module or class, you must include it inside the module or class. So you should just as you mentioned do:

require 'path/b'    
module A
  include B
  def methodA()
    puts methodB.attr1 
  end
end

You should not need to change your test with this since module A already includes module B. However this is not a very good OOP design pattern here. But hopefully you understand why now.

lacostenycoder
  • 10,623
  • 4
  • 31
  • 48
0

After more googling, I found the answer to my 2nd question using suggestion from: https://makandracards.com/makandra/977-calling-selected-methods-of-a-module-from-another-module

so basically i did:

require 'path/b'
module A
   module B_RequiredMethods
       include Path::B
       module_function :methodB
   end

    def methodA
        puts B_RequiredMethods.methodB.attr1
    end
end

In my case, B_RequiredMethods could be named properly to represent the method of B which would be exposed by it. Ideally, I would make those methods class level methods of B, but it is managed by some other team.

phalanx
  • 43
  • 9