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

136

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

Я уверен, что кто-то непременно возьмет трубку и будет догматически утверждать, что «вы должны только использовать общедоступные методы модульного тестирования; если это требует модульного тестирования, это не должен быть защищенный или частный метод», но я не очень заинтересован в обсуждении этого. У меня есть несколько методов, которые являются защищенными или частными по уважительным и обоснованным причинам, эти частные / защищенные методы являются умеренно сложными, и публичные методы в классе зависят от правильного функционирования этих защищенных / частных методов, поэтому мне нужен способ для тестирования Защищенные / частные методы.

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

Брент Чепмен
источник

Ответы:

135

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

myobject.send(:method_name, args)

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

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

Джеймс Бейкер
источник
я думаю, что это использование было отменено в 1.9
Gene T
6
Я сомневаюсь, что они отзовут это, поскольку они немедленно сломают огромное количество проектов рубина
Орион Эдвардс
1
рубин 1,9 делает перерыв почти все.
jes5199
1
Просто на заметку: не берите в голову send!вещь, она была отменена давно, send/__send__может вызывать методы любой видимости - redmine.ruby-lang.org/repositories/revision/1?rev=13824
dolzenko
2
Есть public_send(документация здесь ), если вы хотите соблюдать конфиденциальность. Я думаю, что это плохо для Ruby 1.9.
Эндрю Гримм
71

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

before(:each) do
  MyClass.send(:public, *MyClass.protected_instance_methods)  
end
Уилл Сарджент
источник
9
Да, это здорово. Для частных методов используйте ... private_instance_methods, а не protected_instance_methods
Майк Блит
12
Важное предостережение: это делает методы этого класса общедоступными до конца выполнения набора тестов, что может привести к неожиданным побочным эффектам! Возможно, вы захотите переопределить методы как защищенные снова в блоке after (: each) или столкнуться с пугающими неудачами тестирования в будущем.
Патоген
это ужасно и гениально одновременно
Роберт
Я никогда не видел этого раньше, и я могу засвидетельствовать, что это работает фантастически. Да, это и ужасно, и блестяще, но пока вы ограничиваете его на уровне метода, который вы тестируете, я бы сказал, что у вас не будет неожиданных побочных эффектов, на которые намекает Патоген.
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(), что также даст вам доступ к закрытым и защищенным методам (как предложил Джеймс Бейкер)

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

    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

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

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

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

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

user52804
источник
Да, но большинство Smalltalkers не думали, что это хорошая особенность языка.
aenw
6

Подобно ответу @ 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
Qix
источник
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
tragomaskhalos
источник
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 framework можно написать,

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

Использование send для доступа к защищенным / приватным методам не работает в 1.9, поэтому это не рекомендуемое решение.


источник
1

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

Виктор К.
источник
1
Вы должны добавить комментарий к этому ответу, а не писать новый, чтобы исправить другой.
Зише
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

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

Двоичная логика
источник
14
До сих пор не понимаю эту позицию: да, частные методы являются частными по какой-то причине, но нет, эта причина не имеет ничего общего с тестированием.
Себастьян Фом Меер
Хотелось бы, чтобы я проголосовал за это больше. Единственный правильный ответ в этой теме.
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метод временно редактировать привязку блока, чтобы напомнить целевой объект для объекта-оболочки, но только на время блока. Таким образом, вам не нужно использовать параметр блока, вы можете просто продолжить ссылаться на объект с тем же именем.
Александр - Восстановить Монику