web-dev-qa-db-ja.com

Rubyで保護されたメソッドとプライベートメソッドを単体テストする最良の方法は何ですか?

標準のRuby Test::Unitフレームワークを使用して、Rubyのプロテクトメソッドとプライベートメソッドを単体テストする最良の方法は何ですか?

誰かがパイプして、「パブリックメソッドのみをユニットテストする必要があります。ユニットテストが必要な場合、保護されたメソッドまたはプライベートメソッドであってはなりません」と独断的に断言しますが、私はそれについて議論することにあまり興味がありません。正当かつ正当な理由でare protectedまたはprivateであるメソッドがいくつかあります。これらのprivate/protectedメソッドはやや複雑で、クラスのpublicメソッドはこれらのprotected/privateメソッドが正しく機能することに依存しています。したがって、保護/プライベートメソッドをテストする方法が必要です。

もう1つ...特定のクラスのすべてのメソッドを1つのファイルに入れ、そのクラスのユニットテストを別のファイルに入れます。理想的には、メインソースファイルを可能な限りシンプルで単純な状態に保つために、すべての魔法でこの「保護されたプライベートメソッドのユニットテスト」機能をメインソースファイルではなくユニットテストファイルに実装してください。

131
Brent Chapman

Sendメソッドでカプセル化をバイパスできます。

myobject.send(:method_name, args)

これはRubyの「機能」です。 :)

Ruby 1.9開発中にsendがプライバシーを尊重し、send!無視しますが、最終的にはRuby 1.9で変更されたものはありません。send!と破壊的なもの。

133
James Baker

RSpecを使用する場合の簡単な方法は次のとおりです。

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

テストファイルでクラスを再度開き、1つまたは複数のメソッドをパブリックとして再定義します。メソッド自体の内部を再定義する必要はありません。シンボルをpublic呼び出しに渡すだけです。

元のクラスが次のように定義されている場合:

class MyClass

  private

  def foo
    true
  end
end

テストファイルで、次のようにします。

class MyClass
  public :foo

end

より多くのプライベートメソッドを公開する場合は、publicに複数のシンボルを渡すことができます。

public :foo, :bar
31
Aaron Hinni

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が提案したように)にアクセスすることもできます。

または、テストオブジェクトのメタクラスを変更して、そのオブジェクト専用のprivate/protectedメソッドをパブリックにすることもできます。

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

これにより、そのクラスの他のオブジェクトに影響を与えることなく、これらのメソッドを呼び出すことができます。テストディレクトリ内でクラスを再度開き、テストコード内のすべてのインスタンスに対してクラスをパブリックにすることができますが、パブリックインターフェイスのテストに影響する可能性があります。

10
rampion

私が過去にやったことの1つは次のとおりです。

class foo
  def public_method
    private_method
  end

private unless 'test' == Rails.env

  def private_method
    'private'
  end
end
9
Scott

私は誰かがパイプして、「パブリックメソッドのみをユニットテストする必要があります。ユニットテストが必要な場合、保護されたメソッドまたはプライベートメソッドであってはならない」と独断的に主張しますが、私はそれについて議論することにあまり興味がありません。

また、これらのメソッドがパブリックである新しいオブジェクトにリファクタリングし、元のクラスでプライベートに委任することもできます。これにより、仕様に魔法のメタルービィーを使用せずに、メソッドをプライベートに保ちながらテストできます。

正当かつ正当な理由で保護または非公開になっている方法がいくつかあります

それらの正当な理由は何ですか?その他OOP言語はプライベートメソッドなしで逃げることができます(Smalltalkが思い浮かびます-プライベートメソッドは慣例としてのみ存在します)。

7
user52804

記述されたクラスのすべてのprotectedおよびprivateメソッドを公開するには、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
5
Sean Tan

@WillSargentの応答と同様に、ここでは、FactoryGirlで作成/更新する重いプロセスを経ることなく、いくつかの保護されたバリデータをテストする特別な場合にdescribeブロックで使用したものを使用します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
qix

私はパーティーに遅れていることを知っていますが、プライベートメソッドをテストしないでください....これを行う理由を考えることはできません。パブリックにアクセス可能なメソッドは、そのプライベートメソッドをどこかで使用し、パブリックメソッドと、そのプライベートメソッドが使用される原因となるさまざまなシナリオをテストします。何かが入り、何かが出てきます。プライベートメソッドのテストは大したことではなく、後でコードをリファクタリングするのがはるかに難しくなります。理由はプライベートです。

3
Binary Logic

クラスを「再オープン」して、プライベートメソッドに委任する新しいメソッドを提供できます。

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

Test :: Unitフレームワークで記述できるのは、

MyClass.send(:public, :method_name)

ここで、「method_name」はプライベートメソッドです。

&このメソッドを呼び出すと、

assert_equal expected, MyClass.instance.method_name(params)
2
rahul patil

私はおそらく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
Mike

ここに、私が使用するクラスへの一般的な追加を示します。テストしているメソッドを公開するだけではありませんが、ほとんどの場合、問題ではなく、はるかに読みやすくなっています。

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を使用してprotected/privateメソッドにアクセスしますis 1.9では壊れているため、推奨されるソリューションではありません。

1
Xavier Shay

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は完全に機能します。

1
Franz Hinkel

上記のトップの回答を修正するには、in Ruby 1.9.1、すべてのメッセージを送信するのはObject#sendで、プライバシーを尊重するのはObject#public_sendです。

1
Victor K.

これを行うためには:

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
0
Knut Stenmark