¿Cuál es la mejor manera de probar los métodos protegidos y privados en Ruby?


136

¿Cuál es la mejor manera de probar los métodos protegidos y privados en Ruby, utilizando el Test::Unitmarco estándar de Ruby ?

Estoy seguro de que alguien hablará y afirmará dogmáticamente que "solo debe realizar una prueba unitaria de los métodos públicos; si necesita una prueba unitaria, no debería ser un método protegido o privado", pero no estoy realmente interesado en debatir eso. Tengo varios métodos que están protegidos o privados por razones buenas y válidas, estos métodos privados / protegidos son moderadamente complejos, y los métodos públicos en la clase dependen de que estos métodos protegidos / privados funcionen correctamente, por lo tanto, necesito una forma de probar Los métodos protegidos / privados.

Una cosa más ... Generalmente pongo todos los métodos para una clase dada en un archivo, y las pruebas unitarias para esa clase en otro archivo. Idealmente, me gustaría toda la magia para implementar esta funcionalidad de "prueba unitaria de métodos protegidos y privados" en el archivo de prueba unitaria, no en el archivo fuente principal, para mantener el archivo fuente principal lo más simple y directo posible.


Respuestas:


135

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ó sendrespetar la privacidad e send!ignorarla, pero al final nada cambió en Ruby 1.9. Ignora los comentarios a continuación discutiendo send!y rompiendo cosas.


Creo que este uso fue revocada en 1,9
Gen T

66
Dudo que lo revoquen, ya que romperían instantáneamente una enorme cantidad de proyectos de rubí
Orion Edwards

1
Ruby 1.9 no romper casi todo.
jes5199

1
Sólo para nota: No importa el send!lugar, que fue revocada hace mucho tiempo, send/__send__puede llamar a los métodos de toda la visibilidad - redmine.ruby-lang.org/repositories/revision/1?rev=13824
dolzenko

2
Hay public_send(documentación aquí ) si desea respetar la privacidad. Creo que eso es nuevo para Ruby 1.9.
Andrew Grimm

71

Aquí hay una manera fácil si usa RSpec:

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

9
Sí, eso es genial. Para métodos privados, use ... private_instance_methods en lugar de protected_instance_methods
Mike Blyth

12
Advertencia importante: esto hace que los métodos de esta clase sean públicos para el resto de la ejecución de su conjunto de pruebas, lo que puede tener efectos secundarios inesperados. Es posible que desee redefinir los métodos como protegidos nuevamente en un bloque after (: each) o sufrir fallas de prueba espeluznantes en el futuro.
Patógeno

esto es horrible y brillante al mismo tiempo
Robert

Nunca he visto esto antes y puedo dar fe de que funciona fantásticamente. Sí, es horrible y brillante, pero siempre y cuando lo analice al nivel del método que está probando, diría que no tendrá los efectos secundarios inesperados a los que alude Pathogen.
fuzzygroup

32

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í, solo pase el símbolo en la publicllamada.

Si su clase original se define así:

class MyClass

  private

  def foo
    true
  end
end

En su archivo de prueba, simplemente haga algo como esto:

class MyClass
  public :foo

end

Puede pasar varios símbolos a publicsi desea exponer métodos más privados.

public :foo, :bar

2
Este es mi enfoque preferido, ya que deja su código intacto y simplemente ajusta la privacidad para la prueba específica. No olvide volver a poner las cosas como estaban después de que se hayan ejecutado sus pruebas o podría corromper las pruebas posteriores.
ktec

10

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 send(), que también le dará acceso a métodos privados y protegidos (como sugirió James Baker)

Alternativamente, puede modificar la metaclase de su objeto de prueba para hacer públicos los métodos privados / protegidos 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 hacerlos públicos para todas las instancias dentro de su código de prueba, pero eso podría afectar su prueba de la interfaz pública.


9

Una forma de hacerlo en el pasado es:

class foo
  def public_method
    private_method
  end

private unless 'test' == Rails.env

  def private_method
    'private'
  end
end

8

Estoy seguro de que alguien hablará y afirmará dogmáticamente que "solo debe realizar una prueba unitaria de los métodos públicos; si necesita una prueba unitaria, no debería ser un método protegido o privado", pero no estoy realmente interesado en debatir eso.

También podría refactorizarlos en un nuevo objeto en el que esos métodos son públicos y delegarlos en privado en la clase original. Esto le permitirá probar los métodos sin metaruby mágico en sus especificaciones mientras los mantiene privados.

Tengo varios métodos protegidos o privados por razones válidas y válidas.

¿Cuáles son esas razones válidas? Otros lenguajes de OOP pueden escapar sin métodos privados (smalltalk viene a la mente, donde los métodos privados solo existen como una convención).


Sí, pero la mayoría de los Smalltalkers no creían que esa fuera una buena característica del lenguaje.
aenw

6

Similar a la respuesta de @ WillSargent, esto es lo que he usado en un describebloque para el caso especial de probar algunos validadores protegidos sin necesidad de pasar por el pesado proceso de crearlos / actualizarlos con FactoryGirl (y podría usar de private_instance_methodsmanera 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

5

Para hacer públicos todos los métodos protegidos y privados 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

3

Puede "volver a abrir" la clase y proporcionar un nuevo método que delegue al privado:

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

2

Probablemente me inclinaría hacia el uso de instance_eval (). Sin embargo, antes de saber sobre instance_eval (), creaba una clase derivada en mi archivo de prueba unitaria. Luego establecería los métodos privados para que sean públicos.

En el ejemplo a continuación, el método build_year_range es privado en la clase PublicationSearch :: ISIQuery. Derivar una nueva clase solo con fines de prueba me permite establecer uno o varios métodos para que sean públicos y, por lo tanto, directamente comprobables. 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 crea una instancia de la clase MockISIQuery y prueba directamente el método build_year_range ().


2

En Test :: Unit framework puede escribir,

MyClass.send(:public, :method_name)

Aquí "nombre_método" es un método privado.

y mientras llama a este método puede escribir,

assert_equal expected, MyClass.instance.method_name(params)

1

Aquí hay una adición general a la clase que uso. Es un poco más escopeta que solo hacer público el método que está 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 métodos de envío / acceso protegidos / privados se divide en 1.9, por lo que no es una solución recomendada.


1

Para corregir la respuesta superior anterior: en Ruby 1.9.1, es Object # send el que envía todos los mensajes, y Object # public_send que respeta la privacidad.


1
Debe agregar un comentario a esa respuesta, no escribir una nueva respuesta para corregir otra.
zishe

1

En lugar de obj.send, puede 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, puede usarlos my_private_method_publiclycuando quiera my_private_method.

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

obj.sendpara métodos privados fue reemplazado por send!en 1.9, pero luego send!fue eliminado nuevamente. Entonces obj.sendfunciona perfectamente bien.


1

Sé que llego tarde a la fiesta, pero no pruebes métodos privados ... No puedo pensar en una razón para hacerlo. Un método de acceso público está utilizando ese método privado en alguna parte, pruebe el método público y la variedad de escenarios que causarían el uso de ese método privado. Algo entra, algo sale. Probar métodos privados es un gran no-no, y hace que sea mucho más difícil refactorizar su código más adelante. Son privados por una razón.


14
Aún no entiendo esta posición: Sí, los métodos privados son privados por una razón, pero no, esta razón no tiene nada que ver con las pruebas.
Sebastian vom Meer

Desearía poder votar esto más. La única respuesta correcta en este hilo.
Psynix

Si tiene ese punto de vista, ¿por qué incluso molestarse con las pruebas unitarias? Simplemente escriba las especificaciones de las características: la entrada entra, sale la página, todo lo demás debe estar cubierto, ¿verdad?
ohhh hace

1

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

Heh, me divertí un poco con esto: gist.github.com/amomchilov/ef1c84325fe6bb4ce01e0f0780837a82 Renombrado Disrespecta PrivacyViolator(: P) e hice que el disrespect_privacymétodo edite temporalmente el enlace del bloque, para recordar el objeto objetivo al objeto contenedor, pero solo por la duración de la cuadra. De esa manera no necesita usar un parámetro de bloque, simplemente puede continuar haciendo referencia al objeto con el mismo nombre.
Alexander - Restablece a Monica el
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.