How to undefine class in Ruby?

RubyRuby on-Rails-3Activerecord

Ruby Problem Overview


Undefining a method in Ruby is pretty simple, I can just use undef METHOD_NAME.

Is there anything similar for a class? I am on MRI 1.9.2.

I have to undefine an ActiveRecord Model, run two lines of code, and restore the model back to its original form.

The problem is, I have an model Contact and I am using a company's API and it happens that they have some class called Contact, and changing my model name would be lot of work for me.

What can I do in this situation?

Ruby Solutions


Solution 1 - Ruby

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

EDIT Just noticed your edit, removing the constant is probably not the best way to achieve what you're looking for. Why not just move one of the Contact classes into a separate namespace.

EDIT2 You could also rename your class temporarily like this:

class Foo
  def bar
    'here'
  end
end

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

Again, the trouble with this is that you'll still have the newer Contact class so you'll have to remove that again. I would really recommend name-spacing your classes instead. This will also help you avoid any loading issues

Solution 2 - Ruby

In a similar situation - mocking a class used internally by another class I'm trying to test - I found this to be a workable solution:

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

I would have thought there would be a better way to do this - i.e. I couldn't see a way to pass in the desired return value for reuse - but this seems good enough for now. I'm new to mocking/factories, and I'd love to hear about any alternative methods.

Edit:

Ok, so not so similar after all.

I found a better way using RSpec mock, thanks to an excellent explanation in the RSpec Google Group:

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBhushan LodhaView Question on Stackoverflow
Solution 1 - RubyLee JarvisView Answer on Stackoverflow
Solution 2 - RubyZimbiXView Answer on Stackoverflow