Was ist der beste Weg, um geschützte und private Methoden in Ruby zu testen?

136

Was ist der beste Weg, um geschützte und private Methoden in Ruby unter Verwendung des Standard-Ruby- Test::UnitFrameworks zu testen ?

Ich bin sicher, jemand wird sich melden und dogmatisch behaupten, dass "Sie nur öffentliche Methoden testen sollten; wenn es Unit-Tests benötigt, sollte es keine geschützte oder private Methode sein", aber ich bin nicht wirklich daran interessiert, darüber zu debattieren. Ich habe mehrere Methoden, die aus guten und gültigen Gründen geschützt oder privat sind. Diese privaten / geschützten Methoden sind mäßig komplex, und die öffentlichen Methoden in der Klasse hängen davon ab, dass diese geschützten / privaten Methoden ordnungsgemäß funktionieren. Daher brauche ich eine Möglichkeit zum Testen die geschützten / privaten Methoden.

Noch etwas ... Ich habe im Allgemeinen alle Methoden für eine bestimmte Klasse in einer Datei abgelegt und die Komponententests für diese Klasse in einer anderen Datei. Idealerweise möchte ich die ganze Magie, diese "Unit-Test von geschützten und privaten Methoden" -Funktionalität in die Unit-Test-Datei und nicht in die Hauptquelldatei zu implementieren, um die Hauptquelldatei so einfach und unkompliziert wie möglich zu halten.

Brent Chapman
quelle

Antworten:

135

Sie können die Kapselung mit der Sendemethode umgehen:

myobject.send(:method_name, args)

Dies ist eine 'Funktion' von Ruby. :) :)

Während der Entwicklung von Ruby 1.9 gab es interne Debatten, in denen erwogen wurde, senddie Privatsphäre zu respektieren und zu send!ignorieren, aber am Ende änderte sich in Ruby 1.9 nichts. Ignorieren Sie die folgenden Kommentare send!, um Dinge zu besprechen und zu brechen.

James Baker
quelle
Ich denke, diese Verwendung wurde in 1.9
Gene T
6
Ich bezweifle, dass sie es widerrufen würden, da sie sofort eine enorme Anzahl von Rubinprojekten brechen würden
Orion Edwards
1
Ruby 1.9 nicht brechen so ziemlich alles.
jes5199
1
Nur zur Anmerkung: Egal, die send!Sache, die vor langer Zeit widerrufen wurde, send/__send__kann Methoden aller Sichtbarkeit aufrufen
redmine.ruby-lang.org/repositories/revision/1?rev=13824
2
Es gibt public_send(Dokumentation hier ), wenn Sie die Privatsphäre respektieren möchten. Ich denke, das ist neu in Ruby 1.9.
Andrew Grimm
71

Hier ist eine einfache Möglichkeit, wenn Sie RSpec verwenden:

before(:each) do
  MyClass.send(:public, *MyClass.protected_instance_methods)  
end
Will Sargent
quelle
9
Ja, das ist toll. Verwenden Sie für private Methoden ... private_instance_methods anstelle von protected_instance_methods
Mike Blyth
12
Wichtiger Vorbehalt: Dadurch werden die Methoden in dieser Klasse für den Rest der Ausführung Ihrer Testsuite öffentlich, was unerwartete Nebenwirkungen haben kann! Möglicherweise möchten Sie die Methoden in einem nachfolgenden Block (: jedem) erneut als geschützt definieren oder in Zukunft gruselige Testfehler erleiden.
Pathogen
das ist schrecklich und brillant zugleich
Robert
Ich habe das noch nie gesehen und kann bestätigen, dass es fantastisch funktioniert. Ja, es ist sowohl schrecklich als auch brillant, aber solange Sie es auf der Ebene der Methode, die Sie testen, betrachten, würde ich argumentieren, dass Sie nicht die unerwarteten Nebenwirkungen haben werden, auf die Pathogen anspielt.
Fuzzygroup
32

Öffnen Sie einfach die Klasse in Ihrer Testdatei erneut und definieren Sie die Methode oder Methoden als öffentlich neu. Sie müssen den Mut der Methode selbst nicht neu definieren, sondern nur das Symbol an den publicAufruf übergeben.

Wenn Ihre ursprüngliche Klasse wie folgt definiert ist:

class MyClass

  private

  def foo
    true
  end
end

Führen Sie in Ihrer Testdatei einfach Folgendes aus:

class MyClass
  public :foo

end

Sie können mehrere Symbole übergeben, publicwenn Sie mehr private Methoden verfügbar machen möchten.

public :foo, :bar
Aaron Hinni
quelle
2
Dies ist mein bevorzugter Ansatz, da Ihr Code unberührt bleibt und einfach die Privatsphäre für den jeweiligen Test angepasst wird. Vergessen Sie nicht, die Dinge nach dem Ausführen Ihrer Tests wieder so zu machen, wie sie waren, da Sie sonst spätere Tests beschädigen könnten.
ktec
10

instance_eval() könnte helfen:

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

Sie können damit direkt auf private Methoden und Instanzvariablen zugreifen.

Sie können auch die Verwendung in Betracht ziehen, mit send()der Sie auch auf private und geschützte Methoden zugreifen können (wie von James Baker vorgeschlagen).

Alternativ können Sie die Metaklasse Ihres Testobjekts ändern, um die privaten / geschützten Methoden nur für dieses Objekt öffentlich zu machen.

    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

Auf diese Weise können Sie diese Methoden aufrufen, ohne andere Objekte dieser Klasse zu beeinflussen. Sie können die Klasse in Ihrem Testverzeichnis erneut öffnen und für alle Instanzen in Ihrem Testcode öffentlich machen. Dies kann sich jedoch auf Ihren Test der öffentlichen Schnittstelle auswirken.

Rampion
quelle
9

Eine Möglichkeit, wie ich es in der Vergangenheit gemacht habe, ist:

class foo
  def public_method
    private_method
  end

private unless 'test' == Rails.env

  def private_method
    'private'
  end
end
Scott
quelle
8

Ich bin sicher, jemand wird sich melden und dogmatisch behaupten, dass "Sie nur öffentliche Methoden testen sollten; wenn es Unit-Tests benötigt, sollte es keine geschützte oder private Methode sein", aber ich bin nicht wirklich daran interessiert, darüber zu debattieren.

Sie können diese auch in ein neues Objekt umgestalten, in dem diese Methoden öffentlich sind, und sie in der ursprünglichen Klasse privat an sie delegieren. Auf diese Weise können Sie die Methoden ohne magische Metarubie in Ihren Spezifikationen testen und sie dennoch privat halten.

Ich habe mehrere Methoden, die aus guten und gültigen Gründen geschützt oder privat sind

Was sind diese triftigen Gründe? Andere OOP-Sprachen können überhaupt ohne private Methoden auskommen (Smalltalk fällt mir ein - wo private Methoden nur als Konvention existieren).

user52804
quelle
Ja, aber die meisten Smalltalker hielten das nicht für ein gutes Merkmal der Sprache.
aenw
6

Ähnlich wie bei der Antwort von @ WillSargent habe ich Folgendes in einem describeBlock für den speziellen Fall des Testens einiger geschützter Validatoren verwendet, ohne den Schwergewichtsprozess des Erstellens / Aktualisierens mit FactoryGirl durchlaufen zu müssen (und Sie könnten es private_instance_methodsähnlich verwenden):

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

Um alle geschützten und privaten Methoden für die beschriebene Klasse öffentlich zu machen, können Sie Ihrer spec_helper.rb Folgendes hinzufügen, ohne eine Ihrer Spezifikationsdateien berühren zu müssen.

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
Sean Tan
quelle
3

Sie können die Klasse "erneut öffnen" und eine neue Methode bereitstellen, die an die private delegiert:

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

Ich würde mich wahrscheinlich dazu neigen, instance_eval () zu verwenden. Bevor ich jedoch von instance_eval () wusste, erstellte ich eine abgeleitete Klasse in meiner Unit-Test-Datei. Ich würde dann die private (n) Methode (n) als öffentlich festlegen.

Im folgenden Beispiel ist die Methode build_year_range in der Klasse PublicationSearch :: ISIQuery privat. Durch das Ableiten einer neuen Klasse nur zu Testzwecken kann ich festlegen, dass eine oder mehrere Methoden öffentlich und daher direkt testbar sind. Ebenso macht die abgeleitete Klasse eine Instanzvariable namens 'result' verfügbar, die zuvor nicht verfügbar gemacht wurde.

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

In meinem Komponententest habe ich einen Testfall, der die MockISIQuery-Klasse instanziiert und die build_year_range () -Methode direkt testet.

Mike
quelle
2

Ich weiß, dass ich zu spät zur Party komme, aber teste keine privaten Methoden ... Ich kann mir keinen Grund dafür vorstellen. Eine öffentlich zugängliche Methode verwendet diese private Methode irgendwo, testet die öffentliche Methode und die verschiedenen Szenarien, die dazu führen würden, dass diese private Methode verwendet wird. Etwas geht rein, etwas kommt raus. Das Testen privater Methoden ist ein großes Nein-Nein, und es macht es viel schwieriger, Ihren Code später umzugestalten. Sie sind aus einem bestimmten Grund privat.

Binäre Logik
quelle
14
Verstehe diese Position immer noch nicht: Ja, private Methoden sind aus einem bestimmten Grund privat, aber nein, dieser Grund hat nichts mit Testen zu tun.
Sebastian vom Meer
Ich wünschte, ich könnte dies mehr unterstützen. Die einzig richtige Antwort in diesem Thread.
Psynix
2

In Test :: Unit kann Framework schreiben,

MyClass.send(:public, :method_name)

Hier ist "Methodenname" eine private Methode.

& beim Aufrufen dieser Methode kann schreiben,

assert_equal expected, MyClass.instance.method_name(params)
rahul patil
quelle
1

Hier ist eine allgemeine Ergänzung zu Class, die ich benutze. Es ist ein bisschen mehr Schrotflinte als nur die Methode, die Sie testen, öffentlich zu machen, aber in den meisten Fällen spielt es keine Rolle und es ist viel besser lesbar.

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

Die Verwendung von send für den Zugriff auf geschützte / private Methoden ist in Version 1.9 fehlerhaft und wird daher nicht empfohlen.


quelle
1

Um die obige Top-Antwort zu korrigieren: In Ruby 1.9.1 sendet Object # send alle Nachrichten und Object # public_send respektiert die Privatsphäre.

Victor K.
quelle
1
Sie sollten dieser Antwort einen Kommentar hinzufügen und keine neue Antwort schreiben, um eine andere zu korrigieren.
Zishe
1

Anstelle von obj.send können Sie eine Singleton-Methode verwenden. Es sind 3 weitere Codezeilen in Ihrer Testklasse und es sind keine Änderungen am tatsächlich zu testenden Code erforderlich.

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

In den Testfällen verwenden Sie dann, my_private_method_publiclywann immer Sie testen möchten my_private_method.

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

obj.sendfür private Methoden wurde send!in 1.9 ersetzt, aber später send!wieder entfernt. Funktioniert also obj.sendeinwandfrei.

Franz Hinkel
quelle
1

Um dies zu tun:

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

Sie können dies in Ihrer Datei test_helper implementieren:

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
Knut Stenmark
quelle
Heh, ich hatte ein bisschen Spaß damit: gist.github.com/amomchilov/ef1c84325fe6bb4ce01e0f0780837a82 Umbenannt Disrespectin PrivacyViolator(: P) und die disrespect_privacyMethode hat die Bindung des Blocks vorübergehend bearbeitet, um das Zielobjekt an das Wrapper-Objekt zu erinnern, aber nur für die Dauer des Blocks. Auf diese Weise müssen Sie keinen Blockparameter verwenden. Sie können einfach weiter auf das gleichnamige Objekt verweisen.
Alexander - Monica