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.
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.
Voici un moyen simple si vous utilisez RSpec:
before(:each) do
MyClass.send(:public, *MyClass.protected_instance_methods)
end
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
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.
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
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).
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
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
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.
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
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)
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 ().
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.
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.
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é.
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