Quelle est la meilleure façon de tester les méthodes protégées et privées dans Ruby?

136

Quelle est la meilleure façon de tester des méthodes protégées et privées dans Ruby, en utilisant le Test::Unitframework Ruby standard ?

Je suis sûr que quelqu'un dira dogmatiquement que "vous ne devriez tester que des méthodes publiques unitaires; si cela nécessite des tests unitaires, ce ne devrait pas être une méthode protégée ou privée", mais je ne suis pas vraiment intéressé à en débattre. J'ai plusieurs méthodes qui sont protégées ou privées pour de bonnes et valables raisons, ces méthodes privées / protégées sont modérément complexes, et les méthodes publiques de la classe dépendent du fonctionnement correct de ces méthodes protégées / privées, j'ai donc besoin d'un moyen de tester les méthodes protégées / privées.

Encore une chose ... Je mets généralement toutes les méthodes pour une classe donnée dans un fichier, et les tests unitaires pour cette classe dans un autre fichier. Idéalement, j'aimerais que toute la magie implémente cette fonctionnalité de "test unitaire des méthodes protégées et privées" dans le fichier de test unitaire, pas dans le fichier source principal, afin de garder le fichier source principal aussi simple et direct que possible.

Brent Chapman
la source

Réponses:

135

Vous pouvez contourner l'encapsulation avec la méthode d'envoi:

myobject.send(:method_name, args)

C'est une «fonctionnalité» de Ruby. :)

Il y a eu un débat interne pendant le développement de Ruby 1.9 qui envisageait de sendrespecter la vie privée et de l' send!ignorer, mais au final, rien n'a changé dans Ruby 1.9. Ignorez les commentaires ci-dessous pour discuter send!et casser des choses.

James Baker
la source
Je pense que cet usage a été révoqué dans la version 1.9
Gene T
6
Je doute qu'ils le révoqueraient, car ils casseraient instantanément un nombre énorme de projets ruby
Orion Edwards
1
Ruby 1.9 ne casse à peu près tout.
jes5199
1
Juste à noter: Qu'importe la send!chose, il a été révoqué il y a longtemps, send/__send__peut appeler des méthodes de toute visibilité - redmine.ruby-lang.org/repositories/revision/1?rev=13824
dolzenko
2
Il y a public_send(documentation ici ) si vous voulez respecter la confidentialité. Je pense que c'est nouveau dans Ruby 1.9.
Andrew Grimm
71

Voici un moyen simple si vous utilisez RSpec:

before(:each) do
  MyClass.send(:public, *MyClass.protected_instance_methods)  
end
Will Sargent
la source
9
Oui c'est super. Pour les méthodes privées, utilisez ... private_instance_methods plutôt que protected_instance_methods
Mike Blyth
12
Attention importante: cela rend les méthodes de cette classe publiques pour le reste de l'exécution de votre suite de tests, ce qui peut avoir des effets secondaires inattendus! Vous voudrez peut-être redéfinir les méthodes comme protégées à nouveau dans un bloc after (: each) ou subir des échecs de test effrayants à l'avenir.
Pathogen
c'est horrible et brillant en même temps
Robert
Je n'ai jamais vu cela auparavant et je peux attester que cela fonctionne à merveille. Oui, c'est à la fois horrible et brillant, mais tant que vous le définissez au niveau de la méthode que vous testez, je dirais que vous n'aurez pas les effets secondaires inattendus auxquels Pathogen fait allusion.
fuzzygroup
32

Rouvrez simplement la classe dans votre fichier de test et redéfinissez la ou les méthodes comme publiques. Vous n'avez pas à redéfinir les tripes de la méthode elle-même, il suffit de passer le symbole dans l' publicappel.

Si votre classe d'origine est définie comme ceci:

class MyClass

  private

  def foo
    true
  end
end

Dans votre fichier de test, faites quelque chose comme ceci:

class MyClass
  public :foo

end

Vous pouvez transmettre plusieurs symboles à publicsi vous souhaitez exposer des méthodes plus privées.

public :foo, :bar
Aaron Hinni
la source
2
C'est mon approche préférée car elle laisse votre code intact et ajuste simplement la confidentialité pour le test spécifique. N'oubliez pas de remettre les choses telles qu'elles étaient après l'exécution de vos tests, sinon vous risquez de corrompre les tests ultérieurs.
ktec
10

instance_eval() pourrait aider:

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

Vous pouvez l'utiliser pour accéder directement aux méthodes privées et aux variables d'instance.

Vous pouvez également envisager d'utiliser send(), ce qui vous donnera également accès à des méthodes privées et protégées (comme James Baker l'a suggéré)

Vous pouvez également modifier la métaclasse de votre objet de test pour rendre les méthodes privées / protégées publiques uniquement pour cet objet.

    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

Cela vous permettra d'appeler ces méthodes sans affecter les autres objets de cette classe. Vous pouvez rouvrir la classe dans votre répertoire de test et les rendre publiques pour toutes les instances de votre code de test, mais cela peut affecter votre test de l'interface publique.

rampion
la source
9

Une façon dont je l'ai fait dans le passé est:

class foo
  def public_method
    private_method
  end

private unless 'test' == Rails.env

  def private_method
    'private'
  end
end
Scott
la source
8

Je suis sûr que quelqu'un dira dogmatiquement que "vous ne devriez tester que des méthodes publiques unitaires; si cela nécessite des tests unitaires, ce ne devrait pas être une méthode protégée ou privée", mais je ne suis pas vraiment intéressé à en débattre.

Vous pouvez également les refactoriser dans un nouvel objet dans lequel ces méthodes sont publiques et leur déléguer en privé dans la classe d'origine. Cela vous permettra de tester les méthodes sans métarubie magique dans vos spécifications tout en les gardant privées.

J'ai plusieurs méthodes qui sont protégées ou privées pour de bonnes et valables raisons

Quelles sont ces raisons valables? D'autres langages POO peuvent se passer de méthodes privées du tout (smalltalk vient à l'esprit - là où les méthodes privées n'existent que comme convention).

user52804
la source
Oui, mais la plupart des Smalltalkers ne pensaient pas que c'était une bonne caractéristique de la langue.
aenw
6

Semblable à la réponse de @ WillSargent, voici ce que j'ai utilisé dans un describebloc pour le cas particulier du test de certains validateurs protégés sans avoir à passer par le processus lourd de création / mise à jour avec FactoryGirl (et vous pouvez utiliser de la private_instance_methodsmême manière):

  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
la source
5

Pour rendre publiques toutes les méthodes protégées et privées de la classe décrite, vous pouvez ajouter ce qui suit à votre spec_helper.rb sans avoir à toucher à aucun de vos fichiers de spécifications.

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
la source
3

Vous pouvez «rouvrir» la classe et fournir une nouvelle méthode qui délègue à la classe privée:

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
la source
2

Je pencherais probablement vers l'utilisation de instance_eval (). Avant de connaître instance_eval (), cependant, je créerais une classe dérivée dans mon fichier de test unitaire. Je définirais alors la ou les méthodes privées comme publiques.

Dans l'exemple ci-dessous, la méthode build_year_range est privée dans la classe PublicationSearch :: ISIQuery. Dériver une nouvelle classe uniquement à des fins de test me permet de définir une ou plusieurs méthodes pour qu'elles soient publiques et, par conséquent, directement testables. De même, la classe dérivée expose une variable d'instance appelée «résultat» qui n'était auparavant pas exposée.

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

Dans mon test unitaire, j'ai un cas de test qui instancie la classe MockISIQuery et teste directement la méthode build_year_range ().

Mike
la source
2

Dans Test :: Unit, le framework peut écrire,

MyClass.send(:public, :method_name)

Ici, "nom_méthode" est une méthode privée.

& en appelant cette méthode peut écrire,

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

Voici un ajout général à la classe que j'utilise. C'est un peu plus insensé que de rendre publique la méthode que vous testez, mais dans la plupart des cas, cela n'a pas d'importance et c'est beaucoup plus lisible.

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

L'utilisation d'envoi pour accéder aux méthodes protégées / privées est interrompue dans la version 1.9, ce n'est donc pas une solution recommandée.


la source
1

Pour corriger la première réponse ci-dessus: dans Ruby 1.9.1, c'est Object # send qui envoie tous les messages, et Object # public_send qui respecte la confidentialité.

Victor K.
la source
1
Vous devez ajouter un commentaire à cette réponse, et non écrire une nouvelle réponse pour en corriger une autre.
zishe
1

Au lieu de obj.send, vous pouvez utiliser une méthode singleton. Il s'agit de 3 lignes de code supplémentaires dans votre classe de test et ne nécessite aucune modification du code réel à tester.

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

Dans les cas de test, vous utilisez ensuite my_private_method_publiclychaque fois que vous souhaitez tester my_private_method.

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

obj.sendpour les méthodes privées a été remplacé par send!dans la version 1.9, mais a send!été supprimé ultérieurement . Fonctionne donc obj.sendparfaitement bien.

Franz Hinkel
la source
1

Je sais que je suis en retard à la fête, mais ne testez pas les méthodes privées ... Je ne vois aucune raison de le faire. Une méthode accessible au public utilise cette méthode privée quelque part, teste la méthode publique et la variété de scénarios qui entraîneraient l'utilisation de cette méthode privée. Quelque chose entre, quelque chose sort. Tester des méthodes privées est un grand non-non, et il sera beaucoup plus difficile de refactoriser votre code plus tard. Ils sont privés pour une raison.

Logique binaire
la source
14
Je ne comprends toujours pas cette position: oui, les méthodes privées sont privées pour une raison, mais non, cette raison n'a rien à voir avec les tests.
Sebastian vom Meer
J'aurais aimé pouvoir voter davantage. La seule réponse correcte dans ce fil.
Psynix
Si vous avez ce point de vue, pourquoi même vous embêter avec des tests unitaires? Écrivez simplement les spécifications des fonctionnalités: l'entrée entre, la page sort, tout ce qui se trouve entre les deux doit être couvert, n'est-ce pas?
ohhh le
1

Pour ce faire:

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

Vous pouvez l'implémenter dans votre fichier 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
Knut Stenmark
la source
Hé, je me suis amusé avec ceci: gist.github.com/amomchilov/ef1c84325fe6bb4ce01e0f0780837a82 Renommé Disrespecten PrivacyViolator(: P) et fait que la disrespect_privacyméthode modifie temporairement la liaison du bloc, afin de rappeler l'objet cible à l'objet wrapper, mais uniquement pour la durée du bloc. De cette façon, vous n'avez pas besoin d'utiliser un paramètre de bloc, vous pouvez simplement continuer à référencer l'objet avec le même nom.
Alexander - Réintègre Monica