Object.bypass

Imagine yourself at your computer. You’re crafting an amazing new library. You’ve got a great object model going, and a suite of tests to ensure that each method within your classes behaves as expected. During a refactor you get the feeling that the method you just wrote is getting a bit fat. Since you adhere to the principles of good OO programming and encapsulation, you extract a portion of the method into a private method. Not only does this make the original method much cleaner, it makes the tests cleaner. Breaking a complex method in half allows you to test the behavior of the extracted method separately, and simplifies your testing tasks.

But as you’re refactoring your tests to hit the extracted method directly, you realize that calling the new private method from outside the class is theoretically forbidden, and ugly in practice. Given your private method:

1
2
3
4
5
private

def get_nozzle(type)
  ...
end

You start with this:

1
assert_equal 42, FireTruck.send(:get_nozzle, :big)

Which works fine, but obfuscates the real method call, making it a smidge harder to understand what’s going on. Perhaps this seems trivial–to have to endure such a small leap of understanding as this–but add up a dozen smidges and you get something a little more tedious. After a while, the send call starts to make you angry. “Ruby is such a pretty language, but my code has become the antithesis of beauty!” you proclaim, maddened by what must be done. You even consider making the method public just to avoid the ugliness in your test. Luckily you regain your calm, and are glad that your colleagues can’t hear you think.

If only there was a way to circumvent class privacy and get to that method without having to resort to a visually jarring call to send. Ruby is a powerful language…surely there is a way?!? We’re well practiced in TDD, so we like to envision the ideal interface and work backwards. What if you could do this:

1
assert_equal 42, FireTruck.bypass.get_nozzle(:big)

That would be awesome, right? Turns out you can, and here’s how:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# This allows you to be a good OOP citizen and honor encapsulation, but
# still make calls to private methods (for testing) by doing
#
#   obj.bypass.private_thingie(arg1, arg2)
#
# Which is easier on the eye than
#
#   obj.send(:private_thingie, arg1, arg2)
#
class Object
  class Bypass
    instance_methods.each do |m|
      undef_method m unless m =~ /^__/
    end

    def initialize(ref)
      @ref = ref
    end
  
    def method_missing(sym, *args)
      @ref.__send__(sym, *args)
    end
  end

  def bypass
    Bypass.new(self)
  end
end

Let’s take a look under the hood and see what Object.bypass does for us. First, we open up Object and add a class called Bypass and a method called bypass. Every object will now have a bypass method available on it. When called, it instantiates and returns a new Bypass object with self (self being the object that bypass was called on). Bypass simply stores that reference to self in an instance variable called @ref. In our test code, we call get_nozzle on whatever was returned from bypass (which is a Bypass instance). The instance of Bypass obviously doesn’t have that method, so we implement method_missing to capture any calls for methods that do not exist. However, Object itself contains a number of methods, and should your class contain a method of the same name, we want to call that method, not the one on Object. So at the top of Bypass we have a piece of code that undefines every method on Bypass that doesn’t start with __ (dunder). This leaves us a blank slate, an empty canvas that will cause no name conflicts but still gives us access to __send__ which we use in our method_missing to make the real call to method on the original object!

So feel free to drop this little snippet into your test_helper.rb or similar, and start testing private methods the clean way. You’ll be happy you did!


About this entry