141

What's the best way to unit test protected and private methods in Ruby, using the standard Ruby Test::Unit framework?

I'm sure somebody will pipe up and dogmatically assert that "you should only unit test public methods; if it needs unit testing, it shouldn't be a protected or private method", but I'm not really interested in debating that. I've got several methods that are protected or private for good and valid reasons, these private/protected methods are moderately complex, and the public methods in the class depend upon these protected/private methods functioning correctly, therefore I need a way to test the protected/private methods.

One more thing... I generally put all the methods for a given class in one file, and the unit tests for that class in another file. Ideally, I'd like all the magic to implement this "unit test of protected and private methods" functionality into the unit test file, not the main source file, in order to keep the main source file as simple and straightforward as possible.

Keith Pinson
  • 7,162
  • 5
  • 54
  • 97
Brent Chapman
  • 2,459
  • 3
  • 20
  • 10
  • Possible duplicate of [How do I test a class that has private methods, fields or inner classes?](https://stackoverflow.com/questions/34571/how-do-i-test-a-class-that-has-private-methods-fields-or-inner-classes) – Raedwald Dec 14 '17 at 12:33

16 Answers16

139

You can bypass encapsulation with the send method:

myobject.send(:method_name, args)

This is a 'feature' of Ruby. :)

There was internal debate during Ruby 1.9 development which considered having send respect privacy and send! ignore it, but in the end nothing changed in Ruby 1.9. Ignore the comments below discussing send! and breaking things.

John Bachir
  • 21,401
  • 22
  • 137
  • 203
James Baker
  • 5,748
  • 3
  • 22
  • 28
71

Here's one easy way if you use RSpec:

before(:each) do
  MyClass.send(:public, *MyClass.protected_instance_methods)  
end
zishe
  • 10,055
  • 12
  • 60
  • 101
Will Sargent
  • 4,268
  • 1
  • 29
  • 50
  • 9
    Yes, that's great. For private methods, use ...private_instance_methods rather than protected_instance_methods – Mike Blyth Mar 08 '11 at 13:26
  • 12
    Important caveat: this makes the methods on this class public for the remainder of your test suite execution, which can have unexpected side effects! You may want to redefine the methods as protected again in an after(:each) block or suffer spooky test failures in the future. – Pathogen Oct 27 '15 at 19:15
  • this is horrible and brilliant at the same time – Robert Jan 12 '18 at 14:49
  • I've never seen this before and I can attest that it works fantastically. Yes it is both horrible and brilliant but as long as you scope it at the level of the method you are testing, I would argue that you won't have the unexpected side effects that Pathogen alludes to. – fuzzygroup Apr 29 '19 at 19:30
32

Just reopen the class in your test file, and redefine the method or methods as public. You don't have to redefine the guts of the method itself, just pass the symbol into the public call.

If you original class is defined like this:

class MyClass

  private

  def foo
    true
  end
end

In you test file, just do something like this:

class MyClass
  public :foo

end

You can pass multiple symbols to public if you want to expose more private methods.

public :foo, :bar
Aaron Hinni
  • 14,089
  • 6
  • 37
  • 38
  • 2
    This is my preferred approach as it leaves your code untouched and simply adjusts the privacy for the specific test. Don't forget to put things back the way they were after your tests have run or you might corrupt later tests. – ktec Oct 16 '12 at 13:48
10

instance_eval() might help:

--------------------------------------------------- Object#instance_eval
     obj.instance_eval(string [, filename [, lineno]] )   => obj
     obj.instance_eval {| | block }                       => obj
------------------------------------------------------------------------
     Evaluates a string containing Ruby source code, or the given 
     block, within the context of the receiver (obj). In order to set 
     the context, the variable self is set to obj while the code is 
     executing, giving the code access to obj's instance variables. In 
     the version of instance_eval that takes a String, the optional 
     second and third parameters supply a filename and starting line 
     number that are used when reporting compilation errors.

        class Klass
          def initialize
            @secret = 99
          end
        end
        k = Klass.new
        k.instance_eval { @secret }   #=> 99

You can use it to access private methods and instance variables directly.

You could also consider using send(), which will also give you access to private and protected methods (like James Baker suggested)

Alternatively, you could modify the metaclass of your test object to make the private/protected methods public just for that object.

    test_obj.a_private_method(...) #=> raises NoMethodError
    test_obj.a_protected_method(...) #=> raises NoMethodError
    class << test_obj
        public :a_private_method, :a_protected_method
    end
    test_obj.a_private_method(...) # executes
    test_obj.a_protected_method(...) # executes

    other_test_obj = test.obj.class.new
    other_test_obj.a_private_method(...) #=> raises NoMethodError
    other_test_obj.a_protected_method(...) #=> raises NoMethodError

This will let you call these methods without affecting other objects of that class. You could reopen the class within your test directory and make them public for all the instances within your test code, but that might affect your test of the public interface.

rampion
  • 82,104
  • 41
  • 185
  • 301
9

One way I've done it in the past is:

class foo
  def public_method
    private_method
  end

private unless 'test' == Rails.env

  def private_method
    'private'
  end
end
Scott
  • 6,234
  • 4
  • 22
  • 26
8

I'm sure somebody will pipe up and dogmatically assert that "you should only unit test public methods; if it needs unit testing, it shouldn't be a protected or private method", but I'm not really interested in debating that.

You could also refactor those into a new object in which those methods are public, and delegate to them privately in the original class. This will allow you to test the methods without magic metaruby in your specs while yet keeping them private.

I've got several methods that are protected or private for good and valid reasons

What are those valid reasons? Other OOP languages can get away without private methods at all (smalltalk comes to mind - where private methods only exist as a convention).

user52804
  • 319
  • 2
  • 5
6

Similar to @WillSargent's response, here's what I've used in a describe block for the special case of testing some protected validators without needing to go through the heavyweight process of creating/updating them with FactoryGirl (and you could use private_instance_methods similarly):

  describe "protected custom `validates` methods" do
    # Test these methods directly to avoid needing FactoryGirl.create
    # to trigger before_create, etc.
    before(:all) do
      @protected_methods = MyClass.protected_instance_methods
      MyClass.send(:public, *@protected_methods)
    end
    after(:all) do
      MyClass.send(:protected, *@protected_methods)
      @protected_methods = nil
    end

    # ...do some tests...
  end
qix
  • 6,008
  • 1
  • 44
  • 58
5

To make public all protected and private method for the described class, you can add the following to your spec_helper.rb and not having to touch any of your spec files.

RSpec.configure do |config|
  config.before(:each) do
    described_class.send(:public, *described_class.protected_instance_methods)
    described_class.send(:public, *described_class.private_instance_methods)
  end
end
Sean Tan
  • 194
  • 1
  • 2
4

You can "reopen" the class and provide a new method that delegates to the private one:

class Foo
  private
  def bar; puts "Oi! how did you reach me??"; end
end
# and then
class Foo
  def ah_hah; bar; end
end
# then
Foo.new.ah_hah
tragomaskhalos
  • 2,563
  • 2
  • 16
  • 10
2

I would probably lean toward using instance_eval(). Before I knew about instance_eval(), however, I would create a derived class in my unit test file. I would then set the private method(s) to be public.

In the example below, the build_year_range method is private in the PublicationSearch::ISIQuery class. Deriving a new class just for testing purposes allows me to set a method(s) to be public and, therefore, directly testable. Likewise, the derived class exposes an instance variable called 'result' that was previously not exposed.

# A derived class useful for testing.
class MockISIQuery < PublicationSearch::ISIQuery
    attr_accessor :result
    public :build_year_range
end

In my unit test I have a test case which instantiates the MockISIQuery class and directly tests the build_year_range() method.

Mike
  • 3,373
  • 3
  • 18
  • 12
2

In Test::Unit framework can write,

MyClass.send(:public, :method_name)

Here "method_name" is private method.

& while calling this method can write,

assert_equal expected, MyClass.instance.method_name(params)
rahul patil
  • 619
  • 6
  • 11
1

Instead of obj.send you can use a singleton method. It’s 3 more lines of code in your test class and requires no changes in the actual code to be tested.

def obj.my_private_method_publicly (*args)
  my_private_method(*args)
end

In the test cases you then use my_private_method_publicly whenever you want to test my_private_method.

http://mathandprogramming.blogspot.com/2010/01/ruby-testing-private-methods.html

obj.send for private methods was replaced by send! in 1.9, but later send! was removed again. So obj.send works perfectly well.

Ingo Karkat
  • 154,018
  • 15
  • 205
  • 275
1

To correct the top answer above: in Ruby 1.9.1, it's Object#send that sends all the messages, and Object#public_send that respects privacy.

Victor K.
  • 3,854
  • 3
  • 21
  • 37
1

In order to do this:

disrespect_privacy @object do |p|
  assert p.private_method
end

You can implement this in your test_helper file:

class ActiveSupport::TestCase
  def disrespect_privacy(object_or_class, &block)   # access private methods in a block
    raise ArgumentError, 'Block must be specified' unless block_given?
    yield Disrespect.new(object_or_class)
  end

  class Disrespect
    def initialize(object_or_class)
      @object = object_or_class
    end
    def method_missing(method, *args)
      @object.send(method, *args)
    end
  end
end
  • Heh I had some fun with this: https://gist.github.com/amomchilov/ef1c84325fe6bb4ce01e0f0780837a82 Renamed `Disrespect` to `PrivacyViolator` (:P) and made the `disrespect_privacy` method temporarily edit the block's binding, so as to remind the target object to the wrapper object, but only for the duration of the block. That way you don't need to use a block param, you can just continue referencing the object with the same name. – Alexander Mar 04 '20 at 21:41
1

Here is a general addition to Class which I use. It's a bit more shotgun than only making public the method you are testing, but in most cases it doesn't matter, and it's much more readable.

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

MyClass.publicize_methods do
  assert_equal 10, MyClass.new.secret_private_method
end

Using send to access protected/private methods is broken in 1.9, so is not a recommended solution.

0

I know I'm late to the party, but don't test private methods....I can't think of a reason to do this. A publicly accessible method is using that private method somewhere, test the public method and the variety of scenarios that would cause that private method to be used. Something goes in, something comes out. Testing private methods is a big no-no, and it makes it much harder to refactor your code later. They are private for a reason.

Binary Logic
  • 1,491
  • 1
  • 17
  • 18
  • 15
    Still don't understand this position: Yes, private methods are private for a reason, but no, this reason has nothing to do with testing. – Sebastian vom Meer Jun 26 '13 at 11:47
  • I wish I could upvote this more. The only correct answer in this thread. – Psynix Apr 11 '14 at 01:23
  • 1
    If you have that point of view then why even bother with unit tests? Just write feature specs: input goes in, page comes out, everything in between should be covered right? – ohhh Jul 29 '20 at 11:06