spec - ¿Cuál es la mejor manera de probar la unidad de métodos protegidos y privados en Ruby?
pruebas unitarias ruby (16)
Estoy seguro de que alguien insistirá y dirá dogmáticamente que "usted solo debe probar los métodos públicos de la unidad, si necesita pruebas unitarias, no debe ser un método privado o protegido", pero no estoy realmente interesado en debatir eso.
También podría refactorizarlos en un nuevo objeto en el que esos métodos sean públicos y delegarlos de forma privada en la clase original. Esto le permitirá probar los métodos sin metaruby mágico en sus especificaciones sin dejar de mantenerlos privados.
Tengo varios métodos que están protegidos o privados por buenas y válidas razones
¿Cuáles son esas razones válidas? Otros lenguajes OOP pueden escaparse sin métodos privados en absoluto (Smalltalk viene a la mente, donde los métodos privados solo existen como una convención).
¿Cuál es la mejor manera de probar las unidades de métodos protegidos y privados en Ruby, utilizando el marco estándar de Ruby Test::Unit
?
Estoy seguro de que alguien insistirá y dirá dogmáticamente que "usted solo debe probar los métodos públicos de la unidad, si necesita pruebas unitarias, no debe ser un método privado o protegido", pero no estoy realmente interesado en debatir eso. Tengo varios métodos que están protegidos o son privados por buenas y válidas razones, estos métodos privados / protegidos son moderadamente complejos, y los métodos públicos de la clase dependen de que estos métodos protegidos / privados funcionen correctamente, por lo tanto, necesito una forma de probarlos. los métodos protegidos / privados.
Una cosa más ... Generalmente pongo todos los métodos para una clase dada en un archivo, y la unidad prueba para esa clase en otro archivo. Idealmente, me gustaría que toda la magia implementara esta funcionalidad de "prueba unitaria de métodos privados y protegidos" en el archivo de prueba de la unidad, no en el archivo fuente principal, a fin de mantener el archivo fuente principal lo más simple y directo posible.
Aquí hay una adición general a Class que uso. Es un poco más inteligente que solo hacer público el método que estás probando, pero en la mayoría de los casos no importa, y es mucho más legible.
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
El uso de enviar para acceder a métodos protegidos / privados está roto en 1.9, por lo que no es una solución recomendada.
Aquí hay una manera fácil si usa RSpec:
before(:each) do
MyClass.send(:public, *MyClass.protected_instance_methods)
end
En Test :: unidad marco puede escribir,
MyClass.send(:public, :method_name)
Aquí "method_name" es un método privado.
y mientras llamas a este método puedes escribir,
assert_equal expected, MyClass.instance.method_name(params)
En lugar de obj.send puedes usar un método singleton. Son 3 líneas más de código en su clase de prueba y no requiere cambios en el código real para ser probado.
def obj.my_private_method_publicly (*args)
my_private_method(*args)
end
En los casos de prueba, entonces usa my_private_method_publicly
siempre que quiera probar my_private_method
.
http://mathandprogramming.blogspot.com/2010/01/ruby-testing-private-methods.html
obj.send
para métodos privados fue reemplazado por send!
en 1.9, pero luego send!
fue eliminado de nuevo. Entonces obj.send
funciona perfectamente bien.
Para corregir la respuesta superior anterior: en Ruby 1.9.1, es Object # send que envía todos los mensajes, y Object # public_send que respeta la privacidad.
Para hacer esto:
disrespect_privacy @object do |p|
assert p.private_method
end
Puede implementar esto en su archivo 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
Para hacer público todo el método protegido y privado para la clase descrita, puede agregar lo siguiente a su spec_helper.rb y no tener que tocar ninguno de sus archivos de especificaciones.
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
Probablemente me inclinaría hacia el uso de instance_eval (). Sin embargo, antes de saber sobre instance_eval (), crearía una clase derivada en mi archivo de prueba de unidad. Luego establecería los métodos privados para que sean públicos.
En el siguiente ejemplo, el método build_year_range es privado en la clase PublicationSearch :: ISIQuery. Obtener una nueva clase solo para fines de prueba me permite configurar un método para que sea público y, por lo tanto, directamente comprobable. Del mismo modo, la clase derivada expone una variable de instancia llamada ''resultado'' que anteriormente no estaba expuesta.
# A derived class useful for testing.
class MockISIQuery < PublicationSearch::ISIQuery
attr_accessor :result
public :build_year_range
end
En mi prueba unitaria tengo un caso de prueba que ejemplifica la clase MockISIQuery y prueba directamente el método build_year_range ().
Puede "reabrir" la clase y proporcionar un nuevo método que delegue en la clase privada:
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
Puede omitir la encapsulación con el método de envío:
myobject.send(:method_name, args)
Esta es una ''característica'' de Ruby. :)
¡Hubo un debate interno durante el desarrollo de Ruby 1.9 que consideró send
privacidad del respeto y send!
ignóralo, pero al final nada cambió en Ruby 1.9. Ignora los comentarios a continuación discutiendo send!
y rompiendo cosas
Sé que llego tarde a la fiesta, pero no pruebo métodos privados ... No puedo pensar en una razón para hacer esto. Un método de acceso público es usar ese método privado en alguna parte, probar el método público y la variedad de escenarios que causarían que se use ese método privado. Algo entra, sale algo. Probar métodos privados es un gran no-no, y hace que sea mucho más difícil refactorizar tu código más tarde. Son privados por una razón.
Similar a la respuesta de @ WillSargent, esto es lo que he usado en un bloque de describe
para el caso especial de probar algunos validadores protegidos sin tener que pasar por el proceso pesado de creación / actualización con FactoryGirl (y podría usar private_instance_methods
manera similar):
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
Simplemente vuelva a abrir la clase en su archivo de prueba y redefina el método o los métodos como públicos. No tiene que redefinir las agallas del método en sí, simplemente pase el símbolo a la llamada public
.
Si tu clase original se define así:
class MyClass
private
def foo
true
end
end
En su archivo de prueba, solo haga algo como esto:
class MyClass
public :foo
end
Puede pasar múltiples símbolos a public
si quiere exponer más métodos privados.
public :foo, :bar
Una forma en que lo hice en el pasado es:
class foo
def public_method
private_method
end
private unless ''test'' == Rails.env
def private_method
''private''
end
end
instance_eval()
podría ayudar:
--------------------------------------------------- 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
Puede usarlo para acceder a métodos privados y variables de instancia directamente.
También podría considerar el uso de send()
, que también le dará acceso a métodos privados y protegidos (como sugirió James Baker)
Alternativamente, podría modificar la metaclase de su objeto de prueba para hacer que los métodos privados / protegidos sean públicos solo para ese objeto.
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
Esto le permitirá llamar a estos métodos sin afectar a otros objetos de esa clase. Puede volver a abrir la clase dentro de su directorio de prueba y hacerla pública para todas las instancias dentro de su código de prueba, pero eso podría afectar su prueba de la interfaz pública.