在Ruby中对受保护的私有方法进行单元测试的最佳方法是什么?


136

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

我确定有人会提出并断然断言“您应该仅对公共方法进行单元测试;如果需要进行单元测试,则不应将其作为受保护的方法或私有方法”,但是我对此并不感兴趣。我有几个方法保护的或私有的良好和有效的原因,这些私人/受保护的方法是相对复杂的,并且在类的公共方法依赖于这些保护/私有方法正常工作,所以我需要一种方法来测试受保护/私有方法。

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


Answers:


135

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

myobject.send(:method_name, args)

这是Ruby的“功能”。:)

在Ruby 1.9的开发过程中进行了内部辩论,其中考虑send尊重隐私并send!忽略它,但最终在Ruby 1.9中没有任何改变。忽略下面讨论send!和破坏事情的评论。


我认为此用法已在1.9
Gene T

6
我怀疑他们会撤消它,因为它们会立即破坏大量的红宝石项目
Orion Edwards

1
ruby 1.9 确实破坏了几乎所有东西。
jes5199

1
只需注意:没关系send!,它早已被撤销,send/__send__可以调用所有可见性的方法-redmine.ruby-lang.org/repositories/revision/1?rev=13824
dolzenko 2010年

2
public_send(文件在这里),如果你想尊重他人的隐私。我认为这是Ruby 1.9的新功能。
安德鲁·格林

71

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

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

9
是的,太好了。对于私有方法,请使用... private_instance_methods而不是protected_instance_methods
Mike Blyth

12
重要的警告:这将在测试套件执行的其余部分中公开此类的方法,这可能会带来意外的副作用!您可能希望在after(:each)块中重新定义方法为受保护的方法,否则将来可能会遇到怪异的测试失败。
病原体2015年

这是同时的恐怖和辉煌
罗伯特(Robert Robert)

我以前从未见过此事,我可以证明它工作得非常出色。是的,它既恐怖又辉煌,但是只要您将其调整为所测试方法的水平,我就会认为您不会有Pathogen所暗示的意外副作用。
Fuzzygroup

32

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

如果原始类的定义如下:

class MyClass

  private

  def foo
    true
  end
end

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

class MyClass
  public :foo

end

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

public :foo, :bar

2
这是我的首选方法,因为它可以使您的代码保持不变,并且只需针对特定测试调整隐私。在运行测试后,请不要忘记放回原来的状态,否则您可能会破坏以后的测试。
ktec 2012年

10

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

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


9

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

class foo
  def public_method
    private_method
  end

private unless 'test' == Rails.env

  def private_method
    'private'
  end
end

8

我确定有人会提出并断然断言“您应该仅对公共方法进行单元测试;如果需要进行单元测试,则不应将其作为受保护的方法或私有方法”,但是我对此并不感兴趣。

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

我有几种出于正当理由而受到保护或私有的方法

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


是的,但是大多数Smalltalkers都不认为这是该语言的一个好功能。
aenw

6

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

5

要公开所描述类的所有受保护的私有方法,您可以将以下内容添加到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

3

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

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

2

我可能倾向于使用instance_eval()。但是,在我了解instance_eval()之前,我将在单元测试文件中创建一个派生类。然后,我将私有方法设置为公共方法。

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

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

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


2

在Test :: Unit框架中可以编写,

MyClass.send(:public, :method_name)

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

在调用此方法可以写的同时,

assert_equal expected, MyClass.instance.method_name(params)

1

这是我使用的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

在1.9中,使用send访问受保护的/私有方法已被打破,因此不建议使用此解决方案。


1

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


1
您应该在该答案上添加注释,而不要编写新的答案来更正另一个答案。
zishe 2014年

1

可以使用单例方法来代替obj.send。测试类中还有3行代码,不需要更改要测试的实际代码。

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

然后在测试用例中,my_private_method_publicly每当要进行测试时就使用它my_private_method

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

obj.send私有方法send!在1.9中被替换,但后来又send!被删除。因此obj.send效果很好。


1

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


14
仍然不了解这个立场:是的,私有方法出于某种原因是私有的,但是不,这与测试无关。
Sebastian vom Meer 2013年

我希望我能对此再投票。该线程中唯一正确的答案。
Psynix'4

如果您有这种观点,那为什么还要打扰单元测试呢?只需编写功能说明:输入输入,页面输出,中间的所有内容都应该覆盖正确吗?

1

为此:

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/ef1c84325fe6bb4ce01e0f0780837a82重命名DisrespectPrivacyViolator(:P)并使该disrespect_privacy方法临时编辑块的绑定,以便提醒目标对象到包装对象,但仅在此期间的块。这样,您无需使用块参数,只需继续引用具有相同名称的对象即可。
亚历山大-
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.