What's the best way to unit test protected & private methods in Ruby?

后端 未结 16 592
被撕碎了的回忆
被撕碎了的回忆 2020-12-12 10:47

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 a

相关标签:
16条回答
  • 2020-12-12 11:24

    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).

    0 讨论(0)
  • 2020-12-12 11:24

    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
    
    0 讨论(0)
  • 2020-12-12 11:25

    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
    
    0 讨论(0)
  • 2020-12-12 11:26

    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
    
    0 讨论(0)
  • 2020-12-12 11:28

    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.

    0 讨论(0)
  • 2020-12-12 11:28

    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 讨论(0)
提交回复
热议问题