144

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

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

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

4

16 回答 16

144

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

myobject.send(:method_name, args)

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

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

于 2008-11-06T01:31:58.493 回答
72

Here's one easy way if you use RSpec:

before(:each) do
  MyClass.send(:public, *MyClass.protected_instance_methods)  
end
于 2008-11-06T17:01:32.470 回答
33

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

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

class MyClass

  private

  def foo
    true
  end
end

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

class MyClass
  public :foo

end

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

public :foo, :bar
于 2008-11-06T18:29:52.727 回答
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

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

于 2008-11-06T01:36:17.470 回答
9

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

class foo
  def public_method
    private_method
  end

private unless 'test' == Rails.env

  def private_method
    'private'
  end
end
于 2008-11-06T14:48:23.703 回答
8

我敢肯定,有人会直言不讳地断言“你应该只对公共方法进行单元测试;如果它需要单元测试,它不应该是受保护的或私有的方法”,但我对此并不真正感兴趣。

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

我有几种受保护或私有的方法,这些方法有充分和正当的理由

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

于 2009-07-18T01:19:38.493 回答
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
于 2016-03-03T21:41:06.993 回答
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
于 2012-11-25T14:28:59.780 回答
4

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

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
于 2008-11-06T15:15:18.737 回答
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() 方法。

于 2008-11-06T14:54:16.110 回答
2

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

MyClass.send(:public, :method_name)

这里“method_name”是私有方法。

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

assert_equal expected, MyClass.instance.method_name(params)
于 2015-05-14T18:10:49.973 回答
1

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

于 2010-03-01T22:22:25.110 回答
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 中破坏,因此不是推荐的解决方案。

于 2008-11-25T09:04:03.917 回答
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.sendsend!私有方法在 1.9 中被替换为,但后来又send!被删除了。所以obj.send效果很好。

于 2010-01-06T06:12:07.343 回答
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
于 2015-04-01T11:59:19.567 回答
0

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

于 2013-02-16T23:38:31.087 回答