在 Ruby 中使用单元测试保护和私有方法的最好方法是什么?

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

我确信有人会主动提出并武断地断言“你应该只对公共方法进行单元测试; 如果它需要单元测试,它就不应该是受保护的或私有的方法”,但我对这个问题并不感兴趣。我有几个 保护或私有的方法,这些私有/保护的方法比较复杂,类中的公共方法依赖于这些保护/私有的方法正常工作,因此我需要一种方法来测试保护/私有的方法。

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

40853 次浏览

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

myobject.send(:method_name, args)

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

在 Ruby 1.9的开发过程中,内部存在争议,认为 send尊重隐私,而 send!忽视隐私,但是最终 Ruby 1.9没有任何改变。忽略下面讨论 send!和破坏的评论。

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

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

我过去这么做的一个方法是:

class foo
def public_method
private_method
end


private unless 'test' == Rails.env


def private_method
'private'
end
end

我可能倾向于使用 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 ()方法。

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

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

如果你使用 RSpec,这里有一个简单的方法:

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

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

如果原始类是这样定义的:

class MyClass


private


def foo
true
end
end

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

class MyClass
public :foo


end

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

public :foo, :bar

下面是我使用的 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中断的 protected/private 方法 ,因此不是推荐的解决方案。

我相信会有人说出来的 武断地断言“你应该 只对公共方法进行单元测试; 如果 需要单元测试,它不应该是一个 保护或私人方法”,但我 对辩论不感兴趣 那个。

您还可以将这些方法重构为一个新对象,其中这些方法是公共的,并在原始类中私有地委托它们。这将允许您测试的方法没有神奇的 metaruby 在您的规格,同时保持它们的私有性。

我有几种方法 受到保护或者永远隐私 正当理由

这些合理的理由是什么?其他面向对象语言可以完全不使用私有方法(我想到了 Smalltalk ——其中私有方法只作为约定存在)。

可以使用单例方法代替 objec.send 测试类,并且不需要对要测试的实际代码进行更改。

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

在1.9版中,私有方法的 obj.sendsend!取代,但后来又删除了 send!。所以 obj.send运行得非常好。

为了纠正上面的顶部答案: 在 Ruby 1.9.1中,Object # send 发送所有消息,Object # public _ send 尊重隐私。

要公开所描述类的所有受保护和私有方法,可以向 spec _ helper 添加以下内容。并且不需要触摸任何你的规格文件。

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

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

为了做到这一点:

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

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

MyClass.send(:public, :method_name)

这里的“ method _ name”是私有方法。

& 调用此方法时可以写入,

assert_equal expected, MyClass.instance.method_name(params)

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