In which a weird problem with testing and simply_helpful leads us down the rabbit hole to appreciate the way Ruby modules are included into classes, and how the ordering of such events can be important.
(This is a long one folks; sorry about that)
Ruby is so forgiving. As _why so eloquently stated, “Ruby loves me like Japanese Jesus!”.
I’ve been working in Ruby for, I guess, over five years now, and so I’ve come to take this love for granted. Ruby lets me do whatever I want. Let’s write some code, yeah?
class Monkey def name "Bonobo" end end mike = Monkey.new mike.name
Yeah, real nice. Real sweet monkey there. But hang on, I just realised that I forgot to add a method! Aaaargh!!? I cannot has cheezeburger? :’(
No, wait. This is Ruby, remember? Ruby will forgive me, and let me add the method to the class any time I like!
class Monkey def eat(food) case food when Banana "yum" else throw :poop end end end mike.eat :some_rubbish
Phew! Thank you Ruby! You are so kind! So leniant! You never scold me with a hurtful recompile. Except…
.. it’s not always that simple. Let’s drive over to the car-keys-in-a-punch-bowl mixin party to see how Ruby can sometimes behave in ways less forgiving.
module A end module B include A end module C include B end C.ancestors # => [C, B, A]
The ancestors method shows every module - and I mean every module - that has been “mixed in” to the receiving class or module. So, in this case, we see that C has included B, and also has included A (because B included A). This is obvious when we define some methods in these modules:
module A def a; end end module B include A def b; end end module C include B def c; end end C.instance_methods # => ["a", "c", "b"]
All our methods are there. And, while Ruby smiles kindly down upon us, we can even add methods to those modules afterwards and have them available. Check it:
module A def new_a; end end C.instance_methods # => ["a", "new_a", "c", "b"]
But now watch this. We’re going to build our three modules as before, except that we’re going to include a new module into A after A has been included into B
module A end module B include A end module X end module A include X end B.ancestors # => [B, A]
So you see, even though A now includes X (so we’d expect to see [B, A, X] as the ancestor list), it doesn’t work. We can check the ancestors of module A to be sure that we really did include X in there:
A.ancestors # => [A, X]
Yup, we did. And it gets even hairier. Methods defined on our “missing” module are not available to classes that include B.
module X def hello "dave" end end class D include B # which includes A, which we'd hope (but doesn't) include X end d = D.new d.hello # => NoMethodError: undefined method 'hello' for #<D:0x4ec14>
So here’s the deal - when Ruby included A into B, Ruby internally scans up through all the ancestors of A at that point, tying all of the modules found into the inheritance hierarchy of B to make the methods available to any instances.
However, when we later try and include X into A this has no effect on B, or anything that includes B either, because their inheritance hierarchy’s are already determined. In other words, this inclusion is not dynamic. Oh, Ruby! Why hast thou forsaken us?
A bit of background
The reason I got stuck into this was due to an obscure testing problem that I came across with Rails. When testing views that used the simply_helpful plugin (now a “legacy plugin” since the features have been rolled into edge), I found that helper methods from that plugin were missing, despite working under script/server in any environment:
test_should_show_object(MonkeysControllerTest): ActionView::TemplateError: undefined method 'div_for' for #<#<Class:0x8360ec8>:0x82b8cb4> On line #1 of app/views/monkeys/show.rhtml 1: <% div_for @monkey do %> 2: <b>Name:</b> 3: <%=h @monkey.name %> 4: <% end %>
If we take a look at simply_helpful’s init.rb, we can see that these modules are definitely being declared as helpers, so why aren’t the methods available in the view?
Digging around using the breakpoint mechanism, I discovered the dynamically created class which views are evaluated in. This class is available as self at this point, so let’s examine the modules which are included in it:
The key here is the 6th member of this rowdy band, the charming-yet-anonymously-named #<Module:0x30ffe24>. This module is actually created in ActionController::Base, as the master helper module:
irb:002:0>ActionController::Base.master_helper_module => #<Module:0x30ffe24> irb:003:0> self.class.ancestors => #<Module:0x30ffe24>
When you call helper within a controller (as is happening in simply_helpful’s init.rb), you’re actually mixing in methods and modules to this master helper module.
Furthermore, we can look at the ancestors of this module right here in the view breakpoint:
irb:004:0> self.class.ancestors.ancestors => [#<Module:0x30ffe24>, SimplyHelpful::RecordTagHelper, SimplyHelpful::RecordIdentificationHelper]
Huzzah! There are the simply_helpful modules that we’re seemingly missing. But why aren’t they listed in the ancestors of the self class available to the view? I think you see where we’re going here.
Somehow, and only when testing, and this might not even apply to Edge Rails, the master helper module is being included into the view’s anonymous class before the plugin gets loaded. Weird. Thankfully, there’s a quick-if-dirty fix - we just need to make SURE that these modules get included into the main view class by adding this to the bottom of test/test_helper.rb:
class ActionView::Base include SimplyHelpful::RecordIdentificationHelper, SimplyHelpful::RecordTagHelper end
(Incidentally, this might be a similar problem to this)
(Also: Thanks to David Black for helping me figure this out!)
comments powered by Disqus