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

时间:2008-11-06 00:12:55

标签: ruby unit-testing private protected

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

我确信有人会管道并教条地断言“你应该只测试公共方法;如果它需要单元测试,它不应该是受保护的或私有的方法”,但我真的不感兴趣辩论那个。出于正当和有效的原因,我有几种 受保护或私有的方法,这些私有/受保护的方法是中等复杂的,并且类中的公共方法依赖于这些受保护/私有方法正常运行,因此我需要一种方法来测试受保护/私有方法。

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

16 个答案:

答案 0 :(得分:131)

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

myobject.send(:method_name, args)

这是Ruby的“功能”。 :)

Ruby 1.9开发过程中存在内部争论,认为send尊重隐私,send!忽略它,但最终Ruby 1.9没有任何变化。忽略以下评论,讨论send!和破坏事物。

答案 1 :(得分:71)

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

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

答案 2 :(得分:30)

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

如果原始类定义如下:

class MyClass

  private

  def foo
    true
  end
end

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

class MyClass
  public :foo

end

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

public :foo, :bar

答案 3 :(得分: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

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

答案 4 :(得分:9)

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

class foo
  def public_method
    private_method
  end

private unless 'test' == Rails.env

  def private_method
    'private'
  end
end

答案 5 :(得分:7)

  

我确定有人会说话   教条地断言“你应该   只有单元测试公共方法;如果它   需要单元测试,它不应该是一个   保护或私人方法“,但我   对辩论并不感兴趣   这一点。

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

  

我有几种方法   受保护或私人保护   有效理由

这些有效理由是什么?其他OOP语言可以在没有私有方法的情况下逃脱(想到smalltalk,其中私有方法仅作为约定存在)。

答案 6 :(得分:5)

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

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

答案 7 :(得分:5)

类似于@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 :(得分: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

答案 9 :(得分:3)

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

答案 10 :(得分:2)

我可能倾向于使用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 :(得分:2)

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

MyClass.send(:public, :method_name)

这里“method_name”是私有方法。

&安培;虽然调用此方法可以写,

assert_equal expected, MyClass.instance.method_name(params)

答案 12 :(得分: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

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

答案 13 :(得分: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已被1.9中的send!取代,但后来send!被删除了。所以obj.send非常有效。

答案 14 :(得分:1)

要纠正上面的答案:在Ruby 1.9.1中,它是发送所有消息的Object#send,以及尊重隐私的Object#public_send。

答案 15 :(得分:0)

为了做到这一点:

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