Jaki jest najlepszy sposób testowania jednostkowego chronionych i prywatnych metod w Rubim?


136

Jaki jest najlepszy sposób na testowanie jednostkowe metod chronionych i prywatnych w Rubim przy użyciu standardowego frameworka Rubiego Test::Unit?

Jestem pewien, że ktoś się zepsuje i dogmatycznie zapewni, że „powinieneś testować jednostkowe tylko metody publiczne; jeśli wymaga testów jednostkowych, nie powinno to być metodą chronioną ani prywatną”, ale nie jestem zbyt zainteresowany debatowaniem nad tym. Mam kilka metod, które chronione lub prywatne z dobrych i ważnych powodów, te metody prywatne / chronione są umiarkowanie złożone, a metody publiczne w klasie zależą od tych chronionych / prywatnych metod, które działają poprawnie, dlatego potrzebuję sposobu na przetestowanie metody chronione / prywatne.

Jeszcze jedno ... Generalnie wszystkie metody dla danej klasy umieszczam w jednym pliku, a testy jednostkowe dla tej klasy w innym pliku. Idealnie chciałbym, aby cała magia zaimplementowała tę funkcjonalność „testów jednostkowych metod chronionych i prywatnych” w pliku testu jednostkowego, a nie w głównym pliku źródłowym, aby główny plik źródłowy był tak prosty i bezpośredni, jak to tylko możliwe.


Odpowiedzi:


135

Możesz ominąć hermetyzację metodą wysyłania:

myobject.send(:method_name, args)

To jest „funkcja” Rubiego. :)

Podczas tworzenia Rubiego 1.9 toczyła się wewnętrzna debata, w której rozważano sendposzanowanie prywatności i send!ignorowanie jej, ale ostatecznie nic się nie zmieniło w Rubim 1.9. Zignoruj ​​poniższe komentarze omawiające send!i niszczące rzeczy.


Myślę, że to użycie zostało odwołane w 1.9
Gene T

6
Wątpię, żeby to odwołali, ponieważ natychmiast zerwaliby ogromną liczbę projektów rubinowych
Orion Edwards,

1
Ruby 1.9 nie złamać prawie wszystko.
jes5199

1
Uwaga: nieważne send!, to zostało cofnięte dawno temu, send/__send__można wywołać metody wszelkiej widoczności - redmine.ruby-lang.org/repositories/revision/1?rev=13824
dolzenko

2
Jest public_send(dokumentacja tutaj ), jeśli chcesz, aby uszanować prywatność. Myślę, że to nowość w Rubim 1.9.
Andrew Grimm,

71

Oto jeden łatwy sposób, jeśli używasz RSpec:

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

9
Tak, to wspaniale. Dla metod prywatnych, użytku ... private_instance_methods zamiast protected_instance_methods
Mike Blyth

12
Ważne ostrzeżenie: powoduje to, że metody tej klasy stają się publiczne do końca wykonywania zestawu testów, co może mieć nieoczekiwane skutki uboczne! Możesz ponownie zdefiniować metody jako chronione w bloku after (: each) lub doświadczyć strasznych niepowodzeń testów w przyszłości.
Patogen

to jest jednocześnie straszne i genialne
Robert

Nigdy wcześniej tego nie widziałem i mogę zaświadczyć, że działa fantastycznie. Tak, jest to zarówno okropne, jak i genialne, ale tak długo, jak oceniasz to na poziomie metody, którą testujesz, twierdzę, że nie będziesz miał nieoczekiwanych skutków ubocznych, o których wspomina Pathogen.
fuzzygroup

32

Po prostu otwórz ponownie klasę w pliku testowym i przedefiniuj metodę lub metody jako publiczne. Nie musisz ponownie definiować wnętrzności samej metody, po prostu przekaż symbol do publicwywołania.

Jeśli oryginalna klasa jest zdefiniowana w ten sposób:

class MyClass

  private

  def foo
    true
  end
end

W swoim pliku testowym zrób coś takiego:

class MyClass
  public :foo

end

Możesz przekazać wiele symboli, publicjeśli chcesz ujawnić bardziej prywatne metody.

public :foo, :bar

2
Jest to moje preferowane podejście, ponieważ pozostawia Twój kod nietknięty i po prostu dostosowuje prywatność do konkretnego testu. Nie zapomnij przywrócić stanu rzeczy po uruchomieniu testów, w przeciwnym razie możesz uszkodzić późniejsze testy.
ktec

10

instance_eval() może pomóc:

--------------------------------------------------- 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

Możesz go użyć, aby uzyskać bezpośredni dostęp do metod prywatnych i zmiennych instancji.

Możesz również rozważyć użycie send(), które da ci również dostęp do prywatnych i chronionych metod (jak zasugerował James Baker)

Alternatywnie możesz zmodyfikować metaklasę obiektu testowego, aby uczynić metody prywatne / chronione publicznymi tylko dla tego obiektu.

    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

Umożliwi to wywołanie tych metod bez wpływu na inne obiekty tej klasy. Możesz ponownie otworzyć klasę w katalogu testowym i upublicznić ją dla wszystkich instancji w kodzie testowym, ale może to mieć wpływ na test interfejsu publicznego.


9

Jednym ze sposobów, w jaki zrobiłem to w przeszłości, jest:

class foo
  def public_method
    private_method
  end

private unless 'test' == Rails.env

  def private_method
    'private'
  end
end

8

Jestem pewien, że ktoś się zepsuje i dogmatycznie zapewni, że „powinieneś testować jednostkowe tylko metody publiczne; jeśli wymaga testów jednostkowych, nie powinno to być metodą chronioną ani prywatną”, ale nie jestem zainteresowany debatowaniem nad tym.

Możesz również refaktoryzować je do nowego obiektu, w którym te metody są publiczne, i delegować je prywatnie w oryginalnej klasie. Pozwoli ci to przetestować metody bez magicznej metarubii w twoich specyfikacjach, zachowując ich prywatność.

Mam kilka metod, które są chronione lub prywatne z dobrych i ważnych powodów

Jakie są te ważne powody? Inne języki OOP mogą w ogóle obejść się bez prywatnych metod (przychodzi na myśl smalltalk - gdzie prywatne metody istnieją tylko jako konwencja).


Tak, ale większość Smalltalkerów nie uważała, że ​​to dobra cecha języka.
aenw

6

Podobnie jak w przypadku odpowiedzi @ WillSargent, oto czego użyłem w describebloku w specjalnym przypadku testowania niektórych chronionych walidatorów bez konieczności przechodzenia przez ciężki proces tworzenia / aktualizowania ich za pomocą FactoryGirl (i możesz użyć private_instance_methodspodobnie):

  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

Aby upublicznić wszystkie chronione i prywatne metody dla opisanej klasy, możesz dodać następujący kod do swojego spec_helper.rb i nie musisz dotykać żadnego ze swoich plików specyfikacji.

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

Możesz "ponownie otworzyć" klasę i podać nową metodę, która deleguje do klasy prywatnej:

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

Prawdopodobnie skłaniałbym się ku użyciu instance_eval (). Zanim jednak dowiedziałem się o instance_eval (), utworzyłem klasę pochodną w moim pliku testu jednostkowego. Następnie ustawiłbym prywatne metody jako publiczne.

W poniższym przykładzie metoda build_year_range jest prywatna w klasie PublicationSearch :: ISIQuery. Wyprowadzenie nowej klasy tylko do celów testowych pozwala mi ustawić metodę (y) tak, aby były publiczne, a zatem bezpośrednio testowalne. Podobnie klasa pochodna ujawnia zmienną instancji o nazwie „result”, która wcześniej nie była ujawniona.

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

W moim teście jednostkowym mam przypadek testowy, który tworzy instancję klasy MockISIQuery i bezpośrednio testuje metodę build_year_range ().


2

We frameworku Test :: Unit można pisać,

MyClass.send(:public, :method_name)

Tutaj "nazwa_metody" jest metodą prywatną.

a podczas wywoływania tej metody można pisać,

assert_equal expected, MyClass.instance.method_name(params)

1

Oto ogólny dodatek do Class, którego używam. To trochę bardziej strzelba niż tylko upublicznienie metody, którą testujesz, ale w większości przypadków nie ma to znaczenia i jest znacznie bardziej czytelna.

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

Używanie metody wysyłania w celu uzyskania dostępu do metod chronionych / prywatnych jest zepsute w wersji 1.9, więc nie jest zalecanym rozwiązaniem.


1

Aby poprawić pierwszą odpowiedź powyżej: w Rubim 1.9.1 to Object # send wysyła wszystkie wiadomości, a Object # public_send, który szanuje prywatność.


1
Powinieneś dodać komentarz do tej odpowiedzi, a nie pisać nowej odpowiedzi, aby poprawić inną.
zishe

1

Zamiast obj.send możesz użyć metody singleton. To jeszcze 3 wiersze kodu w twojej klasie testowej i nie wymaga żadnych zmian w rzeczywistym kodzie do przetestowania.

def obj.my_private_method_publicly (*args)
  my_private_method(*args)
end

W przypadkach testowych używasz wtedy, my_private_method_publiclygdy chcesz przetestować my_private_method.

http://mathandprogramming.blogspot.com/2010/01/ruby-testing-private-methods.html

obj.senddla metod prywatnych został zastąpiony send!w 1.9, ale później send!został ponownie usunięty. Więc obj.senddziała doskonale.


1

Wiem, że spóźniłem się na imprezę, ale nie testuj prywatnych metod… Nie mogę wymyślić powodu, żeby to zrobić. Metoda publicznie dostępna polega na użyciu gdzieś tej metody prywatnej, przetestowaniu metody publicznej i różnych scenariuszy, które spowodowałyby użycie tej metody prywatnej. Coś wchodzi, coś wychodzi. Testowanie metod prywatnych to wielkie nie-nie, a później znacznie trudniej jest zrefaktoryzować kod. Nie bez powodu są prywatne.


14
Nadal nie rozumiem tego stanowiska: tak, prywatne metody są prywatne z jakiegoś powodu, ale nie, ten powód nie ma nic wspólnego z testowaniem.
Sebastian vom Meer

Chciałbym móc to bardziej głosować. Jedyna poprawna odpowiedź w tym wątku.
Psynix,

Jeśli masz taki punkt widzenia, to po co zawracać sobie głowę testami jednostkowymi? Po prostu napisz specyfikacje funkcji: dane wejściowe wchodzą, strona wychodzi, wszystko pomiędzy powinno być uwzględnione, prawda?
ohhh

1

W tym celu:

disrespect_privacy @object do |p|
  assert p.private_method
end

Możesz to zaimplementować w swoim pliku 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, trochę się bawiłem z tym: gist.github.com/amomchilov/ef1c84325fe6bb4ce01e0f0780837a82 Zmieniono nazwę Disrespectna PrivacyViolator(: P) i dokonałem disrespect_privacytymczasowej edycji wiązania bloku, aby przypominać obiekt docelowy obiektowi otoki, ale tylko na czas bloku. W ten sposób nie musisz używać parametru bloku, możesz po prostu kontynuować odwoływanie się do obiektu o tej samej nazwie.
Alexander - Przywróć Monikę
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.