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


136

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

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

もう1つ...通常は、特定のクラスのすべてのメソッドを1つのファイルに入れ、そのクラスの単体テストを別のファイルに入れます。理想的には、この「保護されたメソッドとプライベートメソッドの単体テスト」機能をメインソースファイルではなく単体テストファイルに実装して、メインソースファイルをできるだけシンプルかつ単純にするための魔法がすべて欲しいです。


回答:


135

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

myobject.send(:method_name, args)

これがRubyの「特徴」です。:)

Ruby 1.9の開発中にsendプライバシーを尊重し、send!無視することを検討する内部討論がありましたが、結局Ruby 1.9で何も変更されませんでした。send!物事について話し合ったり壊したりする以下のコメントは無視してください。


この使用法は1.9に取り消されたと思います
Gene T

6
私は、彼らはすぐにRubyプロジェクトの膨大な数破るだろうと、彼らはそれを取り消すことはないだろう
オリオンエドワーズ

1
ruby 1.9 ほとんどすべてを壊します。
jes5199 2008年

1
注意してください:気にしないでくださいsend!、それはずっと前に取り消されsend/__send__、すべての可視性のメソッドを呼び出すことができます-redmine.ruby-lang.org/repositories/revision/1?rev=13824
dolzenko

2
ありますpublic_send(ドキュメントはここにあなたがプライバシーを尊重したい場合)。Ruby 1.9の新機能だと思います。
Andrew Grimm

71

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

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

9
はい、それは素晴らしいです。プライベートメソッド、使用... private_instance_methodsいうよりprotected_instance_methodsため
マイク・ブライス

12
重要な注意事項:これにより、残りのテストスイート実行でこのクラスのメソッドがパブリックになり、予期しない副作用が発生する可能性があります。メソッドをafter(:each)ブロックで再び保護されたものとして再定義するか、将来不気味なテストエラーが発生する可能性があります。
病原体、

これは恐ろしいと同時に見事なものです
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

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

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

正当かつ正当な理由で保護されている、またはプライベートなメソッドがいくつかあります

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


はい。しかし、ほとんどのSmalltalkerは、それが言語の優れた機能であるとは考えていませんでした。
aenw 2017

6

@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

記述されたクラスのすべての保護されたプライベートメソッドをパブリックにするには、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クラスでプライベートです。テスト目的でのみ新しいクラスを派生させると、メソッドをパブリックに設定できるため、直接テストできます。同様に、派生クラスは、以前は公開されていなかった「result」と呼ばれるインスタンス変数を公開します。

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

私の単体テストでは、MockISIQueryクラスをインスタンス化し、build_year_range()メソッドを直接テストするテストケースがあります。


2

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


14
それでもこの位置を理解できません:はい、プライベートメソッドは理由によりプライベートですが、いいえ、この理由はテストとは関係ありません。
Sebastian vom Meer 2013年

これをもっと賛成できればいいのに。このスレッドでの唯一の正解。
Psynix 2014

2

Test :: Unitフレームワークでは、

MyClass.send(:public, :method_name)

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

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

assert_equal expected, MyClass.instance.method_name(params)

1

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

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で壊れたので、推奨ソリューションではありません。


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

これを行うためには:

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

Heh私はこれでいくつかの楽しみを持っていました:gist.github.com/amomchilov/ef1c84325fe6bb4ce01e0f0780837a82名前DisrespectPrivacyViolator(:P)に変更され、disrespect_privacyメソッドがブロックのバインディングを一時的に編集して、ターゲットオブジェクトをラッパーオブジェクトに思い出させますが、その間のみブロックの。これにより、ブロックパラメータを使用する必要がなくなり、同じ名前のオブジェクトを参照し続けることができます。
アレクサンダー-モニカ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.