Який найкращий спосіб відстежити захищені та приватні методи в Ruby?


136

Який найкращий спосіб з’єднати тестові захищені та приватні методи в Ruby, використовуючи стандартну Test::Unitрамку Ruby ?

Я впевнений, що хтось відповість і догматично стверджує, що "ви повинні використовувати лише тестування загальнодоступних методів; якщо воно потребує одиничного тестування, воно не повинно бути захищеним чи приватним методом", але мені не дуже цікаво обговорювати це. У мене є кілька методів , які є захищеними або приватними з поважних причин, ці приватні / захищені методи є помірно складними, і відкритими методи в класі залежать від цих охоронюваних / приватних методів функціонування правильно, тому мені потрібен спосіб перевірити захищені / приватні методи.

І ще одне ... Я, як правило, всі методи для даного класу кладу в один файл, а одиничні тести для цього класу - в інший файл. В ідеалі, я хотів би, щоб вся магія була реалізована ця функціональна перевірка захищених та приватних методів у тестовому файлі одиниці, а не в основному вихідному файлі, щоб головний вихідний файл був максимально простим та простим.


Відповіді:


135

Ви можете обійти інкапсуляцію методом відправлення:

myobject.send(:method_name, args)

Це "особливість" Рубі. :)

Під час розробки Ruby 1.9 відбулася внутрішня дискусія, яка вважала, що sendповажає конфіденційність та send!ігнорує її, але врешті в Ruby 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(документація тут ), якщо ви хочете поважати конфіденційність. Я думаю, що це нове для Ruby 1.9.
Ендрю Грімм

71

Ось один простий спосіб, якщо ви використовуєте RSpec:

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

9
Так, це чудово. Для приватних методів використовуйте ... private_instan_methods, а не захищені_instan_methods
Майк Блайт

12
Важливий застереження: це робить методи цього класу загальнодоступними для решти виконання вашого тестового набору, що може мати несподівані побічні ефекти! Ви можете переосмислити методи, як захищені, знову в блоці після (: кожного) або зазнати моторошних помилок тесту в майбутньому.
Збудник

це жахливо і геніально водночас
Роберт

Я ніколи цього не бачив і можу засвідчити, що це працює фантастично. Так, це і жахливо, і геніально, але поки ви застосовуєте його на рівні методу, який ви тестуєте, я б стверджував, що у вас не буде несподіваних побічних ефектів, на які натякає Патоген.
нечітка група

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

Я впевнений, що хтось наголошує і догматично стверджує, що "вам слід проводити лише тестування публічних методів; якщо для цього потрібне тестування одиниць, це не повинно бути захищеним чи приватним методом", але мені не дуже цікаво обговорювати це.

Ви також можете повернути їх до нового об'єкта, в якому ці методи є загальнодоступними, і делегувати їх приватно в оригінальному класі. Це дозволить вам протестувати методи без магічних метарубів у своїх специфікаціях, зберігаючи їх приватними.

У мене є кілька захищених або приватних методів з добрих та вагомих причин

Які ці поважні причини? Інші мови OOP можуть взагалі обійтись без приватних методів (малий розмова приходить на думку - там, де приватні методи існують лише як конвенція).


Так, але більшість Smalltalkers не вважають, що це є гарною рисою мови.
вчора

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

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. Отримання нового класу лише з метою тестування дозволяє мені встановити метод (и) для загального користування і, отже, безпосередньо перевірити. Так само, похідний клас відкриває змінну екземпляра під назвою "результат", яка раніше не піддавалася впливу.

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

У моєму модульному тесті у мене є тестовий випадок, який створює клас MockISIQuery і безпосередньо тестує метод build_year_range ().


2

У тесті :: Frame 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 можна скористатися методом синглтон. Це ще 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
Досі не розумію цієї позиції: Так, приватні методи є приватними з причини, але ні, ця причина не має нічого спільного з тестуванням.
Себастьян vom Meer

Я б хотів, щоб я міг підтримати це ще більше. Єдина правильна відповідь у цій темі.
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метод тимчасово редагувати прив'язку блоку, щоб нагадати цільовий об'єкт для обгорткового об'єкта, але лише на час блоку. Таким чином, вам не потрібно використовувати блок-парам, ви можете просто продовжувати посилатися на об'єкт з тим самим іменем.
Олександр - Відновіть Моніку
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.