web-dev-qa-db-fra.com

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

Quelle est la meilleure façon de tester à l'unité les méthodes protégées et privées dans Ruby, en utilisant la norme Ruby Test::Unit cadre?

Je suis sûr que quelqu'un dénigrera et affirmera de manière dogmatique que "vous ne devriez tester que des méthodes publiques; si cela nécessite des tests unitaires, cela 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 des raisons valables et valides, ces méthodes privées/protégées sont modérément complexes, et les méthodes publiques de la classe dépendent du bon fonctionnement 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.

Une dernière chose ... J'ai généralement mis 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 avoir toute la magie pour implémenter cette fonctionnalité de "test unitaire des méthodes protégées et privées" dans le fichier de test unitaire, pas le fichier source principal, afin de garder le fichier source principal aussi simple et direct que possible.

131
Brent Chapman

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

myobject.send(:method_name, args)

Il s'agit d'une "fonctionnalité" de Ruby. :)

Il y a eu un débat interne pendant le développement de Ruby 1.9 qui a considéré que send respectait la vie privée et send! l'ignorer, mais à la fin rien n'a changé dans Ruby 1.9. Ignorez les commentaires ci-dessous sur send! et casser des choses.

133
James Baker

Voici un moyen simple si vous utilisez RSpec:

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

Rouvrez simplement la classe dans votre fichier de test et redéfinissez la ou les méthodes en tant que public. Vous n'avez pas à redéfinir les tripes de la méthode elle-même, passez simplement le symbole dans l'appel public.

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

class MyClass

  private

  def foo
    true
  end
end

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

class MyClass
  public :foo

end

Vous pouvez passer plusieurs symboles à public si vous souhaitez exposer des méthodes plus privées.

public :foo, :bar
31
Aaron Hinni

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(), qui vous donnera également accès à des méthodes privées et protégées (comme l'a suggéré James Baker)

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 pourrait affecter votre test de l'interface publique.

10
rampion

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

Je suis sûr que quelqu'un dénigrera et affirmera de manière dogmatique que "vous ne devriez tester que des méthodes publiques; s'il a besoin de tests unitaires, cela 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 protégées ou privées pour de bonnes raisons valables

Quelles sont ces raisons valables? D'autres OOP langages peuvent s'en tirer sans méthodes privées du tout (Smalltalk vient à l'esprit - où les méthodes privées n'existent que comme convention).

7
user52804

Pour rendre publique 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
5
Sean Tan

Semblable à la réponse de @ WillSargent, voici ce que j'ai utilisé dans un bloc describe pour le cas spécial de 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 private_instance_methods De même):

  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
qix

Je sais que je suis en retard à la fête, mais ne testez pas les méthodes privées ... Je ne vois pas de 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 gros no-no, et il est beaucoup plus difficile de refactoriser votre code plus tard. Ils sont privés pour une raison.

3
Binary Logic

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

Dans Test :: Framework Framework peut écrire,

MyClass.send(:public, :method_name)

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

& tout en appelant cette méthode peut écrire,

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

Je préférerais probablement utiliser instance_eval (). Avant de connaître instance_eval (), cependant, je créais une classe dérivée dans mon fichier de test unitaire. Je définirais ensuite 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 pas exposée auparavant.

# 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 ().

2
Mike

Voici un ajout général à la classe que j'utilise. C'est un peu plus un fusil de chasse 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

Utiliser l'envoi pour accéder aux méthodes protégées/privées est cassé en 1.9, ce n'est donc pas une solution recommandée.

1
Xavier Shay

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_publicly chaque fois que vous voulez tester my_private_method.

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

obj.send pour les méthodes privées a été remplacé par send! en 1.9, mais plus tard send! a été supprimé à nouveau. Alors obj.send fonctionne parfaitement bien.

1
Franz Hinkel

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

1
Victor K.

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
0
Knut Stenmark