Какой лучший способ для unit test защищенных и приватных методов в Ruby?

Какой лучший способ для unit test защищенных и приватных методов в Ruby, используя стандартную структуру Ruby Test::Unit?

Я уверен, что кто-то будет обсуждать и догматически утверждать, что "вы должны использовать только unit test общедоступные методы, а если это требует модульного тестирования, это не должен быть защищенный или закрытый метод", но мне неинтересно в обсуждении этого. У меня есть несколько методов, которые являются защищенными или приватными по хорошим и обоснованным причинам, эти частные/защищенные методы являются умеренно сложными, а общедоступные методы в классе зависят от этих защищенных/частных методов, поэтому мне нужен способ проверить защищенные/частные методы.

Еще одна вещь... Обычно я помещаю все методы для данного класса в один файл, а модуль тестирует этот класс в другом файле. В идеале я хотел бы, чтобы вся магия реализовала эту функциональность "unit test защищенных и частных методов" в файле unit test, а не в основном исходном файле, чтобы сохранить основной исходный файл максимально простым и понятным.

Ответ 1

Вы можете обходить инкапсуляцию с помощью метода отправки:

myobject.send(:method_name, args)

Это "особенность" Ruby.:)

В ходе разработки Ruby 1.9 произошли внутренние споры, которые считали, что send уважают конфиденциальность, а send! игнорируют его, но в итоге в Ruby 1.9 ничего не изменилось. Игнорируйте приведенные ниже комментарии send! и сломайте вещи.

Ответ 2

Здесь один простой способ, если вы используете RSpec:

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

Ответ 3

Просто заново откройте класс в тестовом файле и переопределите метод или методы как общедоступные. Вам не нужно переопределять кишки самого метода, просто передайте символ в вызов public.

Если исходный класс определен следующим образом:

class MyClass

  private

  def foo
    true
  end
end

В тестовом файле просто сделайте что-то вроде этого:

class MyClass
  public :foo

end

Вы можете передать несколько символов в public, если вы хотите открыть более частные методы.

public :foo, :bar

Ответ 4

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 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(), который также даст вам доступ к закрытым и защищенным методам (например, предложил Джеймс Бейкер)

В качестве альтернативы вы можете изменить метакласс вашего тестового объекта, чтобы сделать частные/защищенные методы общедоступными только для этого объекта.

    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

Это позволит вам вызвать эти методы, не затрагивая другие объекты этого класса. Вы можете повторно открыть класс в своем тестовом каталоге и сделать его общедоступным для всех экземпляры в вашем тестовом коде, но это может повлиять на проверку открытого интерфейса.

Ответ 5

Один из способов, которые я сделал в прошлом, - это:

class foo
  def public_method
    private_method
  end

private unless 'test' == Rails.env

  def private_method
    'private'
  end
end

Ответ 6

Я уверен, что кто- догматически утверждают, что "вы должны только unit test общедоступные методы; если оно требует модульного тестирования, это не должно быть защищенный или закрытый метод", но я не очень заинтересованы в обсуждении что.

Вы также можете реорганизовать их в новый объект, в котором эти методы являются общедоступными, и делегировать их конфиденциально в исходном классе. Это позволит вам тестировать методы без волшебной метарубы в ваших спецификациях, но при этом сохраняя их конфиденциальность.

У меня есть несколько методов, которые защищенными или частными для действительные причины

Каковы эти веские причины? Другие языки ООП могут уйти без частных методов вообще (smalltalk приходит на ум - там, где частные методы существуют только как конвенция).

Ответ 7

Чтобы сделать общедоступным весь защищенный и закрытый метод для описанного класса, вы можете добавить следующее в свой 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

Ответ 8

Вероятно, я склоняюсь к использованию instance_eval(). Однако, прежде чем я узнал о instance_eval(), я бы создал производный класс в моем файле unit test. Затем я бы включил приватный метод (ы).

В приведенном ниже примере метод build_year_range является приватным в классе PublicationSearch:: ISIQuery. Вывод нового класса только для целей тестирования позволяет мне установить метод как общедоступный и, следовательно, непосредственно проверяемый. Аналогично, производный класс предоставляет переменную экземпляра, называемую "результатом", которая ранее не была показана.

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

В моем unit test у меня есть тестовый пример, который создает экземпляр класса MockISIQuery и непосредственно проверяет метод build_year_range().

Ответ 9

Вы можете "повторно открыть" класс и предоставить новый метод, который делегирует частному:

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

Ответ 10

В модуле Test:: Unit можно писать,

MyClass.send(:public, :method_name)

Здесь "method_name" - частный метод.

& при вызове этого метода можно написать

assert_equal expected, MyClass.instance.method_name(params)

Ответ 11

Подобно ответу @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

Ответ 12

Вот общее дополнение к классу, который я использую. Это немного больше дробовика, чем обнародовать метод, который вы тестируете, но в большинстве случаев это не имеет значения, и это гораздо более читаемо.

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, поэтому это не рекомендуется.

Ответ 13

Чтобы исправить верхний ответ выше: в Ruby 1.9.1 отправляется Object # send, который отправляет все сообщения, и Object # public_send, которые уважают конфиденциальность.

Ответ 14

Я знаю, что опаздываю на вечеринку, но не проверяю частные методы... Я не могу придумать причину этого. Общедоступный метод использует этот частный метод где-то, проверяет общедоступный метод и множество сценариев, которые могли бы использовать этот частный метод. Что-то происходит, что-то выходит. Тестирование частных методов - это большой нет-нет, и это значительно усложняет реорганизацию вашего кода позже. Они являются частными по какой-то причине.

Ответ 15

Вместо 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 работает отлично.

Ответ 16

Чтобы сделать это:

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