Qual é a melhor maneira de testar métodos protegidos e privados em Ruby?

136

Qual é a melhor maneira de testar métodos privados e protegidos no Ruby, usando a Test::Unitestrutura padrão do Ruby ?

Tenho certeza de que alguém se manifestará e dogmaticamente afirmará que "você deve apenas testar métodos públicos por unidade; se precisar de testes unitários, não deve ser um método protegido ou privado", mas não estou realmente interessado em debater isso. Eu tenho vários métodos que são protegidos ou privados por boas e válidas razões, esses métodos privados / protegidos são moderadamente complexos e os métodos públicos da classe dependem desses métodos protegidos / privados funcionando corretamente, portanto, preciso de uma maneira de testar os métodos protegidos / privados.

Mais uma coisa ... Eu geralmente coloco todos os métodos para uma determinada classe em um arquivo e os testes de unidade para essa classe em outro arquivo. Idealmente, eu gostaria que toda a mágica implementasse essa funcionalidade de "teste de unidade de métodos protegidos e privados" no arquivo de teste de unidade, não no arquivo de origem principal, para manter o arquivo de origem principal o mais simples e direto possível.

Brent Chapman
fonte

Respostas:

135

Você pode ignorar o encapsulamento com o método send:

myobject.send(:method_name, args)

Este é um 'recurso' do Ruby. :)

Houve um debate interno durante o desenvolvimento do Ruby 1.9 que considerou sendrespeitar a privacidade e send!ignorá-la, mas no final nada mudou no Ruby 1.9. Ignore os comentários abaixo discutindo send!e quebrando coisas.

James Baker
fonte
eu acho que esse uso foi revogada em 1,9
Gene T
6
Duvido que eles o revogassem, pois quebrariam instantaneamente um enorme número de projetos em rubi
Orion Edwards
1
Ruby 1.9 que quebrar quase tudo.
jes5199
1
Só para nota: Não importa a send!coisa, foi revogada há muito tempo, send/__send__pode chamar métodos de toda a visibilidade - redmine.ruby-lang.org/repositories/revision/1?rev=13824
dolzenko
2
Existe public_send(documentação aqui ) se você deseja respeitar a privacidade. Eu acho que isso é novo no Ruby 1.9.
Andrew Grimm
71

Aqui está uma maneira fácil se você usar o RSpec:

before(:each) do
  MyClass.send(:public, *MyClass.protected_instance_methods)  
end
Will Sargent
fonte
9
Sim, isso é ótimo. Para métodos particulares, uso ... private_instance_methods em vez de protected_instance_methods
Mike Blyth
12
Advertência importante: isso torna públicos os métodos dessa classe para o restante da execução do seu conjunto de testes, o que pode ter efeitos colaterais inesperados! Você pode redefinir os métodos como protegidos novamente em um bloco depois (: cada) ou sofrer falhas de teste assustadoras no futuro.
Pathogen
isto é horrível e brilhante ao mesmo tempo
Robert
Eu nunca vi isso antes e posso atestar que funciona de maneira fantástica. Sim, é horrível e brilhante, mas contanto que você a inclua no nível do método que está testando, eu diria que você não terá os efeitos colaterais inesperados aos quais o Pathogen alude.
Fuzzygroup 29/04/19
32

Apenas reabra a classe no seu arquivo de teste e redefina o método ou métodos como público. Você não precisa redefinir a coragem do próprio método, basta passar o símbolo para a publicchamada.

Se sua classe original é definida assim:

class MyClass

  private

  def foo
    true
  end
end

No arquivo de teste, faça algo assim:

class MyClass
  public :foo

end

Você pode passar vários símbolos para publicse quiser expor mais métodos particulares.

public :foo, :bar
Aaron Hinni
fonte
2
Essa é minha abordagem preferida, pois deixa seu código intocado e simplesmente ajusta a privacidade para o teste específico. Não se esqueça de colocar as coisas de volta do jeito que estavam depois que seus testes foram executados ou você pode corromper os testes posteriores.
KTEC
10

instance_eval() pode ajudar:

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

Você pode usá-lo para acessar métodos privados e variáveis ​​de instância diretamente.

Você também pode considerar o uso send(), o que também lhe dará acesso a métodos privados e protegidos (como sugeriu James Baker)

Como alternativa, você pode modificar a metaclasse do seu objeto de teste para tornar públicos os métodos privados / protegidos apenas para esse 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

Isso permitirá que você chame esses métodos sem afetar outros objetos dessa classe. Você pode reabrir a classe em seu diretório de teste e torná-la pública para todas as instâncias em seu código de teste, mas isso pode afetar seu teste da interface pública.

rampion
fonte
9

Uma maneira de fazer isso no passado é:

class foo
  def public_method
    private_method
  end

private unless 'test' == Rails.env

  def private_method
    'private'
  end
end
Scott
fonte
8

Tenho certeza de que alguém se manifestará e dogmaticamente afirmará que "você deve apenas testar métodos públicos por unidade; se precisar de testes unitários, não deve ser um método protegido ou privado", mas não estou realmente interessado em debater isso.

Você também pode refatorá-los em um novo objeto no qual esses métodos são públicos e delegá-los em particular na classe original. Isso permitirá que você teste os métodos sem metaruby mágico em suas especificações, mantendo-os privados.

Eu tenho vários métodos que são protegidos ou privados por boas e válidas razões

Quais são esses motivos válidos? Outras linguagens OOP podem sair sem métodos privados (o smalltalk vem à mente - onde métodos privados existem apenas como uma convenção).

user52804
fonte
Sim, mas a maioria dos Smalltalkers não achou essa uma boa característica do idioma.
aenw
6

Semelhante à resposta do @ WillSargent, eis o que eu usei em um describebloco para o caso especial de testar alguns validadores protegidos sem precisar passar pelo processo pesado de criar / atualizar com o FactoryGirl (e você pode usar o private_instance_methodsmesmo):

  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
qix
fonte
5

Para tornar público todo o método protegido e privado da classe descrita, você pode adicionar o seguinte ao seu spec_helper.rb e não precisar tocar em nenhum dos seus arquivos de especificação.

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
Sean Tan
fonte
3

Você pode "reabrir" a classe e fornecer um novo método que delega para o 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
tragomaskhalos
fonte
2

Eu provavelmente me inclinaria a usar instance_eval (). Antes que eu soubesse sobre instance_eval (), no entanto, criaria uma classe derivada no meu arquivo de teste de unidade. Eu definiria o (s) método (s) privado (s) para público.

No exemplo abaixo, o método build_year_range é privado na classe PublicationSearch :: ISIQuery. A obtenção de uma nova classe apenas para fins de teste permite definir um método para ser público e, portanto, diretamente testável. Da mesma forma, a classe derivada expõe uma variável de instância chamada 'resultado' que não foi exposta anteriormente.

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

No meu teste de unidade, tenho um caso de teste que instancia a classe MockISIQuery e testa diretamente o método build_year_range ().

Mike
fonte
2

Sei que estou atrasado para a festa, mas não teste métodos privados ... Não consigo pensar em um motivo para fazer isso. Um método acessível ao público está usando esse método privado em algum lugar, teste o método público e a variedade de cenários que causariam o uso desse método privado. Algo entra, sai algo. Testar métodos privados é um grande não-não, e torna muito mais difícil refatorar seu código posteriormente. Eles são particulares por um motivo.

Lógica Binária
fonte
14
Ainda não entendo esta posição: Sim, os métodos privados são privados por um motivo, mas não, esse motivo não tem nada a ver com o teste.
Sebastian vom Meer
Eu gostaria de poder votar mais isso. A única resposta correta neste tópico.
Psynix 11/04
2

Na estrutura Test :: Unit pode escrever,

MyClass.send(:public, :method_name)

Aqui "method_name" é um método privado.

e ao chamar esse método pode escrever,

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

Aqui está uma adição geral à classe que eu uso. É um pouco mais complicado do que apenas tornar público o método que você está testando, mas na maioria dos casos isso não importa e é muito mais legível.

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

O uso do envio para acessar métodos protegidos / privados é quebrado em 1.9, portanto, não é uma solução recomendada.


fonte
1

Para corrigir a resposta principal acima: no Ruby 1.9.1, é o Object # send que envia todas as mensagens e o Object # public_send que respeita a privacidade.

Victor K.
fonte
1
Você deve adicionar um comentário a essa resposta, não escrevendo uma nova resposta para corrigir outra.
Zishe 6/06/2014
1

Em vez de obj.send, você pode usar um método singleton. São mais três linhas de código na sua classe de teste e não requer alterações no código real a ser testado.

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

Nos casos de teste, você usa my_private_method_publiclysempre que deseja testar my_private_method.

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

obj.sendpara métodos privados foi substituído por send!1.9, mas mais tarde send!foi removido novamente. Então, obj.sendfunciona perfeitamente bem.

Franz Hinkel
fonte
1

Para fazer isso:

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

Você pode implementar isso no seu arquivo 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
Knut Stenmark
fonte
Eu me diverti um pouco com isso: gist.github.com/amomchilov/ef1c84325fe6bb4ce01e0f0780837a82 Renomeado Disrespectpara PrivacyViolator(: P) e fiz o disrespect_privacymétodo editar temporariamente a ligação do bloco, de modo a lembrar o objeto de destino para o objeto do wrapper, mas apenas pela duração do bloco. Dessa forma, você não precisa usar um parâmetro de bloco, basta continuar referenciando o objeto com o mesmo nome.
Alexander - Restabelecer Monica