Qual è il modo migliore per testare unitamente metodi protetti e privati ​​in Ruby?


136

Qual è il modo migliore per testare unitamente metodi protetti e privati ​​in Ruby, usando il Test::Unitframework Ruby standard ?

Sono sicuro che qualcuno farà pipeline e affermerà dogmaticamente che "dovresti testare solo metodi pubblici unitari; se ha bisogno di test unitari, non dovrebbe essere un metodo protetto o privato", ma non sono davvero interessato a discuterne. Ho diversi metodi che sono protetti o privati ​​per ragioni valide e valide, questi metodi privato / protetto sono moderatamente complessi e i metodi pubblici nella classe dipendono dal fatto che questi metodi protetti / privati ​​funzionino correttamente, quindi ho bisogno di un modo per testare i metodi protetti / privati.

Un'altra cosa ... In genere inserisco tutti i metodi per una determinata classe in un file e l'unità testa per quella classe in un altro file. Idealmente, vorrei che tutta la magia implementasse questa funzionalità "unit test di metodi protetti e privati" nel file di unit test, non nel file sorgente principale, al fine di mantenere il file sorgente principale il più semplice e diretto possibile.


Risposte:


135

Puoi bypassare l'incapsulamento con il metodo send:

myobject.send(:method_name, args)

Questa è una "caratteristica" di Ruby. :)

Ci fu un dibattito interno durante lo sviluppo di Ruby 1.9 che considerava il sendrispetto della privacy e lo send!ignorava, ma alla fine nulla è cambiato in Ruby 1.9. Ignora i commenti qui sotto discutendo send!e rompendo le cose.


penso che questo uso sia stato revocato in 1.9
Gene T

6
Dubito che lo revocherebbero, poiché interromperebbero immediatamente un numero enorme di progetti rubini
Orion Edwards,

1
ruby 1.9 non rompe praticamente tutto.
jes5199,

1
Solo per notare: non importa la send!cosa, è stata revocata molto tempo fa, send/__send__può chiamare metodi di tutta visibilità - redmine.ruby-lang.org/repositories/revision/1?rev=13824
dolzenko,

2
C'è public_send(documentazione qui ) se si desidera rispettare la privacy. Penso che sia nuovo per Ruby 1.9.
Andrew Grimm,

71

Ecco un modo semplice se usi RSpec:

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

9
Sì, è grandioso. Per i metodi privati, usa ... private_instance_methods piuttosto che protect_instance_methods
Mike Blyth

12
Avvertenza importante: questo rende pubblici i metodi di questa classe per il resto dell'esecuzione della suite di test, che può avere effetti collaterali imprevisti! Potresti voler ridefinire i metodi come protetti di nuovo in un blocco successivo (: ogni) o subire fallimenti di test spettrali in futuro.
Pathogen,

questo è orribile e geniale allo stesso tempo
Robert,

Non l'ho mai visto prima e posso attestare che funziona in modo fantastico. Sì, è sia orribile che geniale, ma fintanto che lo si analizza a livello del metodo che si sta testando, direi che non avrai gli effetti collaterali inaspettati a cui allude Pathogen.
fuzzygroup,

32

Riapri la classe nel tuo file di test e ridefinisci il metodo o i metodi come pubblici. Non è necessario ridefinire le viscere del metodo stesso, basta passare il simbolo inpublic chiamata.

Se la tua classe originale è definita in questo modo:

class MyClass

  private

  def foo
    true
  end
end

Nel tuo file di test, fai qualcosa del genere:

class MyClass
  public :foo

end

È possibile passare più simboli a publicse si desidera esporre più metodi privati.

public :foo, :bar

2
Questo è il mio approccio preferito in quanto lascia intatto il codice e regola semplicemente la privacy per il test specifico. Non dimenticare di rimettere le cose com'erano dopo l'esecuzione dei test o potresti corrompere i test successivi.
ktec,

10

instance_eval() potrebbe aiutare:

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

Puoi usarlo per accedere direttamente a metodi privati ​​e variabili di istanza.

Potresti anche prendere in considerazione l'utilizzo send(), che ti darà anche accesso a metodi privati ​​e protetti (come suggerito da James Baker)

In alternativa, è possibile modificare la metaclasse dell'oggetto test per rendere pubblici i metodi privati ​​/ protetti solo per quell'oggetto.

    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

Questo ti permetterà di chiamare questi metodi senza influenzare altri oggetti di quella classe. È possibile riaprire la classe nella directory di test e renderla pubblica per tutte le istanze all'interno del codice di test, ma ciò potrebbe influire sul test dell'interfaccia pubblica.


9

Un modo in cui l'ho fatto in passato è:

class foo
  def public_method
    private_method
  end

private unless 'test' == Rails.env

  def private_method
    'private'
  end
end

8

Sono sicuro che qualcuno farà pipeline e affermerà dogmaticamente che "dovresti testare solo metodi pubblici unitari; se ha bisogno di test unitari, non dovrebbe essere un metodo protetto o privato", ma non sono davvero interessato a discuterne.

Potresti anche riformattare quelli in un nuovo oggetto in cui tali metodi sono pubblici e delegare loro privatamente nella classe originale. Ciò ti consentirà di testare i metodi senza metarubia magica nelle tue specifiche pur mantenendoli privati.

Ho diversi metodi che sono protetti o privati ​​per motivi validi e validi

Quali sono questi validi motivi? Altre lingue OOP possono cavarsela senza metodi privati ​​(mi viene in mente smalltalk - dove i metodi privati ​​esistono solo come una convenzione).


Sì, ma la maggior parte dei Smalltalker non pensava che fosse una buona caratteristica della lingua.
anche il

6

Simile alla risposta di @ WillSargent, ecco cosa ho usato in un describeblocco per il caso speciale di testare alcuni validatori protetti senza dover passare attraverso il pesante processo di creazione / aggiornamento con FactoryGirl (e potresti usarlo in private_instance_methodsmodo simile):

  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

Per rendere pubblico tutto il metodo protetto e privato per la classe descritta, puoi aggiungere quanto segue a spec_helper.rb e non dover toccare nessuno dei tuoi file delle specifiche.

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

Puoi "riaprire" la classe e fornire un nuovo metodo che delega a quello privato:

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

Probabilmente mi spingerei ad usare instance_eval (). Prima di sapere di instance_eval (), tuttavia, avrei creato una classe derivata nel mio file di test dell'unità. Vorrei quindi impostare i metodi privati ​​come pubblici.

Nell'esempio seguente, il metodo build_year_range è privato nella classe PublicationSearch :: ISIQuery. Derivare una nuova classe solo a scopo di test mi consente di impostare un metodo (i) per essere pubblico e, quindi, direttamente testabile. Allo stesso modo, la classe derivata espone una variabile di istanza chiamata 'risultato' che non era precedentemente esposta.

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

Nel mio unit test ho un caso di test che crea un'istanza della classe MockISIQuery e verifica direttamente il metodo build_year_range ().


2

In Test :: Unit framework può scrivere,

MyClass.send(:public, :method_name)

Qui "method_name" è un metodo privato.

e mentre chiami questo metodo puoi scrivere,

assert_equal expected, MyClass.instance.method_name(params)

1

Ecco un'aggiunta generale alla classe che uso. È un po 'più fucile che rendere pubblico il metodo che stai testando, ma nella maggior parte dei casi non importa ed è molto più leggibile.

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'uso di send per accedere a metodi protetti / privati è stato interrotto in 1.9, quindi non è una soluzione consigliata.


1

Per correggere la risposta principale sopra: in Ruby 1.9.1, è Object # send che invia tutti i messaggi e Object # public_send che rispetta la privacy.


1
È necessario aggiungere un commento a quella risposta, non scrivere una nuova risposta per correggerne un'altra.
Il

1

Invece di obj.send puoi usare un metodo singleton. Sono altre 3 righe di codice nella classe di test e non richiede alcuna modifica nel codice effettivo per essere testate.

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

Nei casi di test si utilizza quindi my_private_method_publiclyogni volta che si desidera eseguire il test my_private_method.

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

obj.sendper i metodi privati ​​è stato sostituito da send!1.9, ma successivamente è send!stato rimosso di nuovo. Quindi obj.sendfunziona perfettamente.


1

So di essere in ritardo alla festa, ma non testare metodi privati ​​.... Non riesco a pensare a un motivo per farlo. Un metodo accessibile pubblicamente sta usando quel metodo privato da qualche parte, testare il metodo pubblico e la varietà di scenari che causerebbero l'uso di quel metodo privato. Qualcosa entra, qualcosa esce. Testare metodi privati ​​è un grande no-no e rende molto più difficile il refactoring del codice in un secondo momento. Sono privati ​​per un motivo.


14
Non capisco ancora questa posizione: sì, i metodi privati ​​sono privati ​​per un motivo, ma no, questo motivo non ha nulla a che fare con i test.
Sebastian vom Meer,

Vorrei poter votare di più. L'unica risposta corretta in questa discussione.
Psynix,

Se hai questo punto di vista, perché perdere tempo con i test unitari? Basta scrivere le specifiche delle funzionalità: l'input entra, la pagina esce, tutto ciò che sta nel mezzo dovrebbe essere coperto, giusto?
ohhh

1

Per fare ciò:

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

Puoi implementarlo nel tuo file 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

Mi sono divertito un po 'con questo: gist.github.com/amomchilov/ef1c84325fe6bb4ce01e0f0780837a82 Rinominato Disrespectin PrivacyViolator(: P) e fatto in modo che il disrespect_privacymetodo modifichi temporaneamente il legame del blocco, in modo da ricordare l'oggetto target all'oggetto wrapper, ma solo per la durata del blocco. In questo modo non è necessario utilizzare un parametro di blocco, è possibile continuare a fare riferimento all'oggetto con lo stesso nome.
Alexander - Ripristina Monica
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.