Ruby에서 보호 및 개인 메소드를 단위 테스트하는 가장 좋은 방법은 무엇입니까?


136

표준 Ruby Test::Unit프레임 워크를 사용하여 Ruby에서 보호 및 개인 메소드를 단위 테스트하는 가장 좋은 방법은 무엇입니까 ?

누군가 공개적으로 분석 할 때는 "공용 메소드 만 단위 테스트해야합니다. 단위 테스트가 필요한 경우 보호 된 방법이나 비공개 방법이 아니어야합니다"라고 확신하지만 실제로는 그 논쟁에 관심이 없습니다. 나는 여러 가지 방법을 가지고 있습니다 보호 또는 좋은 유효한 이유로 개인을, 그러므로 내가 테스트하는 방법이 필요합니다, 이러한 개인 / 보호 방법은 적당히 복잡하고, 클래스의 public 메소드가 제대로 작동이 보호 / 개인 방법에 의존 보호 / 비공개 방법

한가지 더 ... 나는 일반적으로 주어진 클래스에 대한 모든 메소드를 하나의 파일에 넣고, 유닛은 그 클래스에 대한 다른 파일을 테스트합니다. 이상적으로는 주 소스 파일을 가능한 한 간단하고 간단하게 유지하기 위해이 "보호 및 개인 메소드의 단위 테스트"기능을 주 소스 파일이 아닌 단위 테스트 파일에 구현하는 것이 가장 좋습니다.


답변:


135

send 메소드를 사용하여 캡슐화를 우회 할 수 있습니다.

myobject.send(:method_name, args)

이것은 루비의 '기능'입니다. :)

루비 1.9 개발 과정에서 send프라이버시 를 존중하고 send!무시 하는 것을 고려한 내부 논쟁이 있었지만, 결국 루비 1.9에서는 아무것도 바뀌지 않았습니다. 아래 내용을 논의 send!하고 깨뜨리는 의견은 무시하십시오 .


나는이 사용법이 1.9에서 취소되었다고 생각한다
Gene T

6
그들이 엄청난 수의 루비 프로젝트를 즉시 중단했기 때문에 그들이 그것을 철회하지 않을 것입니다.
Orion Edwards

1
루비 1.9 거의 모든 것을 깨뜨립니다.
jes5199

1
참고 사항 : send!오래 전에 해지 된 것을 신경 쓰지 마십시오. send/__send__모든 가시성의 메소드를 호출 할 수 있습니다 -redmine.ruby-lang.org/repositories/revision/1?rev=13824
dolzenko

2
있다 public_send(설명서는 여기에 당신의 개인 정보를 존중하려는 경우). 루비 1.9의 새로운 기능이라고 생각합니다.
앤드류 그림

71

RSpec을 사용하는 쉬운 방법은 다음과 같습니다.

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

9
예, 훌륭합니다. 개인 메소드의 경우 protected_instance_methods 대신 ... private_instance_methods를 사용하십시오.
Mike Blyth

12
중요한 경고 :이 클래스의 메소드는 테스트 스위트 실행의 나머지 부분에 공개되어 예기치 않은 부작용이 발생할 수 있습니다! after (: each) 블록에서 다시 보호 된대로 메소드를 다시 정의하거나 나중에 테스트 실패에 시달릴 수 있습니다.
병원체

이것은 동시에 끔찍하고 훌륭합니다
Robert

나는 이것을 전에 본 적이 없으며 그것이 훌륭하게 작동한다는 것을 증명할 수 있습니다. 예, 그것은 끔찍하고 훌륭하지만 테스트하는 방법의 수준에서 범위를 지정하면 병원체가 암시하는 예기치 않은 부작용이 없을 것이라고 주장합니다.
fuzzygroup

32

테스트 파일에서 클래스를 다시 열고 메소드를 공개로 다시 정의하십시오. 메소드 자체의 내장을 재정의 할 필요는 없으며 심볼을 public호출에 전달하면됩니다 .

원래 클래스가 다음과 같이 정의 된 경우 :

class MyClass

  private

  def foo
    true
  end
end

테스트 파일에서 다음과 같이하십시오.

class MyClass
  public :foo

end

public더 많은 개인용 메소드를 노출하려는 경우 여러 기호를 전달할 수 있습니다 .

public :foo, :bar

2
이것은 코드를 그대로 유지하고 특정 테스트에 대한 개인 정보를 조정하기 때문에 선호하는 접근 방식입니다. 테스트 실행 후의 상태로 되돌 리거나 나중에 테스트를 손상시킬 수 있습니다.
ktec

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 언어는 개인 메소드 없이도 얻을 수 있습니다 (개인 메소드는 규칙으로 만 존재하는 스몰 토크가 떠 오릅니다).


그렇습니다. 그러나 대부분의 스몰 토커들은 그것이 언어의 좋은 기능이라고 생각하지 않았습니다.
aenw

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

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

1

obj.send 대신 singleton 메소드를 사용할 수 있습니다. 테스트 클래스에는 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

나는 파티에 늦었다는 것을 알고 있지만 개인적인 방법을 테스트하지 않습니다 .... 나는 이것을 할 이유를 생각할 수 없습니다. 공개적으로 액세스 가능한 방법은 해당 개인 방법을 어딘가에서 사용하고, 공개 방법 및 해당 개인 방법을 사용하게하는 다양한 시나리오를 테스트합니다. 무언가가 들어오고 뭔가가 나옵니다. 전용 메소드 테스트는 큰 문제가되지 않으며 나중에 코드를 리팩토링하기가 훨씬 어렵습니다. 그들은 사유로 사적입니다.


14
여전히이 입장을 이해하지 못합니다. 예, 전용 메소드는 사유로 인해 비공개이지만,이 이유는 테스트와 관련이 없습니다.
Sebastian vom Meer 2016 년

나는 이것을 더 많이 투표 할 수 있으면 좋겠다. 이 스레드에서 유일한 정답입니다.
Psynix

그 관점이 있다면 왜 단위 테스트를 방해합니까? 기능 사양을 작성하십시오. 입력이 들어오고, 페이지가 나옵니다. 사이에있는 모든 것이 올바르게 다루어 져야합니까?
오의

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

: ㅎ 좀이 재미 있었다 gist.github.com/amomchilov/ef1c84325fe6bb4ce01e0f0780837a82 명칭 변경 Disrespect에를PrivacyViolator 하고했다 (P) disrespect_privacy만 기간 동안, 랩퍼 오브젝트에 대상 개체를 생각 나게하기 때문에 같은 방법은 일시적으로 블록의 바인딩 편집 블록의. 이렇게하면 블록 매개 변수를 사용할 필요가 없으며 같은 이름으로 객체를 계속 참조하면됩니다.
알렉산더-복원 모니카
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.