web-dev-qa-db-fra.com

Comment annuler la définition d'une classe en Ruby?

Définir une méthode en Ruby est assez simple, je peux simplement utiliser undef METHOD_NAME.

Y a-t-il quelque chose de similaire pour une classe? Je suis sur MRI 1.9.2.

Je dois annuler la définition d'un modèle ActiveRecord, exécuter deux lignes de code et restaurer le modèle dans sa forme d'origine.

Le problème est que j'ai un modèle Contact et j'utilise l'API d'une entreprise et il se trouve qu'ils ont une classe appelée Contact, et changer le nom de mon modèle serait beaucoup de travail pour moi.

Que puis-je faire dans cette situation?

53
Bhushan Lodha
>> class Foo; end
=> nil
>> Object.constants.include?(:Foo)
=> true
>> Object.send(:remove_const, :Foo)
=> Foo
>> Object.constants.include?(:Foo)
=> false
>> Foo
NameError: uninitialized constant Foo

MODIFIER Je viens de remarquer votre modification, la suppression de la constante n'est probablement pas la meilleure façon de réaliser ce que vous recherchez. Pourquoi ne pas simplement déplacer l'une des classes Contact dans un espace de noms séparé.

EDIT2 Vous pouvez également renommer temporairement votre classe comme ceci:

class Foo
  def bar
    'here'
  end
end

TemporaryFoo = Foo
Object.send(:remove_const, :Foo)
# do some stuff
Foo = TemporaryFoo
Foo.new.bar #=> "here"

Encore une fois, le problème avec cela est que vous aurez toujours la nouvelle classe Contact, vous devrez donc la supprimer à nouveau. Je recommanderais vraiment d'espacer les noms de vos classes à la place. Cela vous aidera également à éviter tout problème de chargement

84
Lee Jarvis

Dans une situation similaire - se moquant d'une classe utilisée en interne par une autre classe que j'essaie de tester - j'ai trouvé que c'était une solution viable:

describe TilesAuth::Communicator do
  class FakeTCPSocket
    def initialize(*_); end
    def puts(*_); end
  end

  context "when the response is SUCCESS" do
    before do
      class TilesAuth::Communicator::TCPSocket < FakeTCPSocket
        def gets; 'SUCCESS'; end
      end
    end
    after { TilesAuth::Communicator.send :remove_const, :TCPSocket }

    it "returns success" do
      communicator = TilesAuth::Communicator.new Host: nil, port: nil, timeout: 0.2
      response = communicator.call({})
      expect(response["success"]).to eq(true)
      expect(response).not_to have_key("error")
      expect(response).not_to have_key("invalid_response")
    end
  end
end

J'aurais pensé qu'il y aurait une meilleure façon de le faire - c'est-à-dire que je ne pouvais pas voir un moyen de transmettre la valeur de retour souhaitée pour la réutilisation - mais cela semble assez bon pour l'instant. Je suis nouveau dans les moqueries/usines, et j'aimerais entendre parler de toutes les méthodes alternatives.

Éditer:

Ok, donc pas si similaire après tout.

J'ai trouvé une meilleure façon d'utiliser la maquette RSpec, grâce à ne excellente explication dans le groupe Google RSpec :

context "with socket response mocked" do
  let(:response) do
    tcp_socket_object = instance_double("TCPSocket", puts: nil, gets: socket_response)
    class_double("TCPSocket", new: tcp_socket_object).as_stubbed_const
    communicator = TilesAuth::Communicator.new Host: nil, port: nil, timeout: 0.2
    communicator.call({})
  end

  context "as invalid JSON" do
    let(:socket_response) { 'test invalid json' }

    it "returns an error response including the invalid socket response" do
      expect(response["success"]).to eq(false)
      expect(response).to have_key("error")
      expect(response["invalid_response"]).to eq(socket_response)
    end
  end

  context "as SUCCESS" do
    let(:socket_response) { 'SUCCESS' }

    it "returns success" do
      expect(response["success"]).to eq(true)
      expect(response).not_to have_key("error")
      expect(response).not_to have_key("invalid_response")
    end
  end
end
1
ZimbiX