【问题标题】:What's the best way to unit test protected & private methods in Ruby?在 Ruby 中对受保护方法和私有方法进行单元测试的最佳方法是什么?
【发布时间】:2010-09-21 00:01:57
【问题描述】:

使用标准 Ruby Test::Unit 框架在 Ruby 中对受保护方法和私有方法进行单元测试的最佳方法是什么?

我敢肯定,有人会直言不讳地断言“你应该只对公共方法进行单元测试;如果它需要单元测试,它不应该是受保护的或私有的方法”,但我对辩论那个。我有几个保护或私有的方法,出于充分和正当的理由,这些私有/受保护的方法相当复杂,并且类中的公共方法依赖于这些受保护/私有方法的正常运行,因此我需要一种方法来测试受保护/私有方法。

还有一件事...我通常将给定类的所有方法放在一个文件中,并将该类的单元测试放在另一个文件中。理想情况下,我希望将这种“受保护和私有方法的单元测试”功能实现到单元测试文件中,而不是主源文件中,以使主源文件尽可能简单明了。

【问题讨论】:

标签: ruby unit-testing private protected


【解决方案1】:

您可以使用 send 方法绕过封装:

myobject.send(:method_name, args)

这是 Ruby 的一个“特性”。 :)

在 Ruby 1.9 开发期间存在内部争论,考虑让 send 尊重隐私而 send! 忽略它,但最终 Ruby 1.9 没有任何改变。忽略下面讨论 send! 和破坏事物的 cmets。

【讨论】:

  • 我认为这个用法在 1.9 中被撤销了
  • 我怀疑他们会撤销它,因为他们会立即破坏大量的 ruby​​ 项目
  • ruby 1.9 确实破坏了一切。
  • 请注意:不要介意 send! 的事情,它早就被撤销了,send/__send__ 可以调用所有可见性的方法 - redmine.ruby-lang.org/repositories/revision/1?rev=13824
  • 如果您想尊重隐私,可以使用public_send(文档here)。我认为这对 Ruby 1.9 来说是新的。
【解决方案2】:

如果您使用 RSpec,这是一种简单的方法:

before(:each) do
  MyClass.send(:public, *MyClass.protected_instance_methods)  
end

【讨论】:

  • 是的,这很棒。对于私有方法,使用 ...private_instance_methods 而不是 protected_instance_methods
  • 重要警告:这使得这个类的方法在你的测试套件执行的其余部分公开,这可能会产生意想不到的副作用!您可能希望在 after(:each) 块中再次将方法重新定义为受保护的,否则将来会出现令人毛骨悚然的测试失败。
  • 这既可怕又精彩
  • 我以前从未见过这个,我可以证明它非常有效。是的,它既可怕又出色,但只要你在测试方法的层面上进行范围界定,我认为你不会有 Pathogen 所暗示的意外副作用。
【解决方案3】:

只需重新打开测试文件中的类,然后将方法重新定义为公共的。您不必重新定义方法本身的内容,只需将符号传递给 public 调用即可。

如果你原来的类是这样定义的:

class MyClass

  private

  def foo
    true
  end
end

在您的测试文件中,只需执行以下操作:

class MyClass
  public :foo

end

如果您想公开更多私有方法,可以将多个符号传递给public

public :foo, :bar

【讨论】:

  • 这是我的首选方法,因为它不会影响您的代码,并且只需调整特定测试的隐私。不要忘记在您的测试运行后将它们恢复到原来的样子,否则您可能会破坏以后的测试。
【解决方案4】:

instance_eval() 可能会有所帮助:

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

您可以使用它直接访问私有方法和实例变量。

您也可以考虑使用send(),它还可以让您访问私有和受保护的方法(就像 James Baker 建议的那样)

或者,您可以修改测试对象的元类,使私有/受保护的方法仅为该对象公开。

    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

这将让您调用这些方法而不影响该类的其他对象。 你可以在你的测试目录中重新打开这个类,并将它们公开给所有 测试代码中的实例,但这可能会影响您对公共接口的测试。

【讨论】:

    【解决方案5】:

    我过去做过的一种方法是:

    class foo
      def public_method
        private_method
      end
    
    private unless 'test' == Rails.env
    
      def private_method
        'private'
      end
    end
    

    【讨论】:

      【解决方案6】:

      我敢肯定有人会 教条地断言“你应该 仅对公共方法进行单元测试;如果它 需要单元测试,它不应该是 受保护或私有方法”,但我是 对辩论不感兴趣 那个。

      您还可以将它们重构为一个新对象,其中这些方法是公共的,并在原始类中私下委托给它们。这将允许您在规范中测试没有魔法元数据的方法,同时保持它们的私密性。

      我有几种方法 永久保护或私有 正当理由

      这些正当理由是什么?其他 OOP 语言可以完全不使用私有方法(想到 smalltalk - 其中私有方法仅作为约定存在)。

      【讨论】:

      • 是的,但大多数 Smalltalkers 并不认为这是该语言的一个好特性。
      【解决方案7】:

      与@WillSargent 的回复类似,这是我在describe 块中使用的特殊情况,用于测试一些受保护的验证器,而无需经历使用FactoryGirl 创建/更新它们的重量级过程(您可以使用private_instance_methods 类似):

        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
      

      【讨论】:

        【解决方案8】:

        要公开所描述类的所有受保护和私有方法,您可以将以下内容添加到您的 spec_helper.rb 中,而不必修改您的任何规范文件。

        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
        

        【讨论】:

          【解决方案9】:

          您可以“重新打开”该类并提供一个委托给私有类的新方法:

          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
          

          【讨论】:

            【解决方案10】:

            我可能倾向于使用 instance_eval()。然而,在我知道 instance_eval() 之前,我会在我的单元测试文件中创建一个派生类。然后我会将私有方法设置为公开的。

            在下面的示例中,build_year_range 方法在 PublicationSearch::ISIQuery 类中是私有的。仅出于测试目的派生一个新类允许我将一个或多个方法设置为公开的,因此可以直接测试。同样,派生类公开了一个名为“result”的实例变量,该变量以前未公开。

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

            在我的单元测试中,我有一个实例化 MockISIQuery 类并直接测试 build_year_range() 方法的测试用例。

            【讨论】:

              【解决方案11】:

              在Test::Unit框架可以写,

              MyClass.send(:public, :method_name)
              

              这里的“method_name”是私有方法。

              &同时调用这个方法可以写,

              assert_equal expected, MyClass.instance.method_name(params)
              

              【讨论】:

                【解决方案12】:

                这是我使用的 Class 的一般补充。这比仅仅公开你正在测试的方法要多一些,但在大多数情况下它并不重要,而且它更具可读性。

                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
                

                使用 send 访问受保护/私有方法在 1.9 中被破坏,因此不是推荐的解决方案。

                【讨论】:

                  【解决方案13】:

                  更正上面的最佳答案:在 Ruby 1.9.1 中,发送所有消息的是 Object#send,尊重隐私的是 Object#public_send。

                  【讨论】:

                  • 您应该对该答案添加评论,而不是编写新答案来纠正另一个答案。
                  【解决方案14】:

                  您可以使用单例方法代替 obj.send。你的代码中多了 3 行代码 测试类,并且不需要更改要测试的实际代码。

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

                  然后,在测试用例中,只要您想测试my_private_method,就使用my_private_method_publicly

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

                  私有方法的obj.send 在1.9 中被send! 取代,但后来send! 又被删除了。所以obj.send 工作得很好。

                  【讨论】:

                    【解决方案15】:

                    为了做到这一点:

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

                    你可以在你的 test_helper 文件中实现它:

                    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
                    

                    【讨论】:

                    • 嘿嘿,我玩了这个:gist.github.com/amomchilov/ef1c84325fe6bb4ce01e0f0780837a82Disrespect 重命名为PrivacyViolator (:P) 并让disrespect_privacy 方法临时编辑块的绑定,以提醒目标对象到包装器对象,但仅限于块的持续时间。这样你就不需要使用块参数,你可以继续引用同名的对象。
                    【解决方案16】:

                    我知道我迟到了,但不要测试私有方法....我想不出这样做的理由。可公开访问的方法是在某处使用该私有方法,测试该公共方法以及可能导致使用该私有方法的各种场景。有东西进去,有东西出来。测试私有方法是一个很大的禁忌,它使以后重构代码变得更加困难。它们是私有的是有原因的。

                    【讨论】:

                    • 还是不明白这个立场:是的,私有方法之所以私有是有原因的,但是不,这个原因与测试无关。
                    • 我希望我能更多地支持这个。此线程中唯一正确的答案。
                    • 如果您有这种观点,那么为什么还要费心进行单元测试呢?只写功能规范:输入进去,页面出来,中间的一切都应该被覆盖吧?
                    猜你喜欢
                    • 2012-01-09
                    • 2015-02-26
                    • 1970-01-01
                    • 1970-01-01
                    • 2011-08-01
                    • 1970-01-01
                    • 2017-06-24
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多