Monday, November 12, 2007

Ruby: Testing Private Methods

Jason Rudolph recently wrote about Testing Private Methods in Ruby. Before I continue, it's probably a good idea to note that I rarely test private methods. I prefer to test through the public API. However, there are times when life is easier if you write a few tests for a private method or two.

The solution highlighted by Jason (original implementation from Evan Phoenix) is nice from a maintainability perspective since it encapsulates changing the behavior of the class within the test. However, it's a bit verbose for my taste.

Reading the entry I couldn't help but think that gaining temporary access to a private method for testing seems like something that can be handled by a general solution instead.

Here's how I would have solved the problem.

require 'rubygems'
require 'dust'
require 'test/unit'

class Ninja
private
def kill(num_victims)
"#{num_victims} victims are no longer with us."
end
end

class Class
def publicize_methods
saved_private_instance_methods = self.private_instance_methods
self.class_eval { public *saved_private_instance_methods }
yield
self.class_eval { private *saved_private_instance_methods }
end
end

unit_tests do
test "kill returns a murder string" do
Ninja.publicize_methods do
assert_equal '3 victims are no longer with us.', Ninja.new.kill(3)
end
end
end

7 comments:

  1. Anonymous2:38 PM

    In this case, why not just use send()?

    ReplyDelete
  2. I believe the context of the conversation was how would you test in Ruby 1.9 since send will no longer allow you to call private methods.

    ReplyDelete
  3. Hi Jay,

    I was wondering, where would you put the publicize_methods class? Is it wise to have this in your normal code or should this be for testing only?

    Also, your way is very slick and is less verbose. To me though, the other technique makes it a bit easier at a glance what it going on with the test. I will have to use both techniques to see which ends up winning.

    Thanks!

    ReplyDelete
  4. DrMark,

    Lately I've been putting these type of methods in a *_extensions files (class_extensions for this example). You can put these files in an extension folder under the test directory.

    Cheers, Jay

    ReplyDelete
  5. Any idea how to get rid of this warning?

    " `*' interpreted as argument prefix"

    ReplyDelete
  6. Just enclose the argument in parenthesis.

    class Class
    def publicize_methods
    saved_private_instance_methods = self.private_instance_methods
    self.class_eval { public(*saved_private_instance_methods) }
    yield
    self.class_eval { private(*saved_private_instance_methods) }
    end
    end

    ReplyDelete
  7. Hi Jay,

    I needed to test some private class methods so created a modified version for that purpose.

    ReplyDelete

Note: Only a member of this blog may post a comment.