Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Resetting a singleton instance in Ruby

Tags:

How do I reset a singleton object in Ruby? I know that one'd never want to do this in real code but what about unit tests?

Here's what I am trying to do in an RSpec test -

describe MySingleton, "#not_initialised" do   it "raises an exception" do     expect {MySingleton.get_something}.to raise_error(RuntimeError)   end end 

It fails because one of my previous tests initialises the singleton object. I have tried following Ian White's advice from this link which essentially monkey patches Singleton to provide a reset_instance method but I get an undefined method 'reset_instance' exception.

require 'singleton'  class <<Singleton   def included_with_reset(klass)     included_without_reset(klass)     class <<klass       def reset_instance         Singleton.send :__init__, self         self       end     end   end   alias_method :included_without_reset, :included   alias_method :included, :included_with_reset end  describe MySingleton, "#not_initialised" do   it "raises an exception" do     MySingleton.reset_instance     expect {MySingleton.get_something}.to raise_error(RuntimeError)   end end 

What is the most idiomatic way to do this in Ruby?

like image 317
thegreendroid Avatar asked Aug 26 '12 03:08

thegreendroid


People also ask

Can we Deinit a singleton object?

If you have a regular object that you can't deinitialize it's a memory problem. Singletons are no different, except that you have to write a function to do it. Singletons have to be completely self managed. This means from init to deinit.

How do I delete a singleton instance?

Hence, user cannot delete the singleton instance using the keyword “delete”. Also, we have to introduce a static method in the singleton class, say “releaseInstance() that will be used to delete singleton class pointer. Introduce a new static variable “count” that will be used to track users.

How do you clear a singleton instance in Swift?

You don't destroy a singleton. A singleton is created the first time anyone needs it, and is never destroyed as long as the application lives.

What is singleton Ruby?

Singleton is a creational design pattern, which ensures that only one object of its kind exists and provides a single point of access to it for any other code. Singleton has almost the same pros and cons as global variables. Although they're super-handy, they break the modularity of your code.


2 Answers

I guess simply do this will fix your problem:

describe MySingleton, "#not_initialised" do   it "raises an exception" do     Singleton.__init__(MySingleton)     expect {MySingleton.get_something}.to raise_error(RuntimeError)   end end 

or even better add to before callback:

describe MySingleton, "#not_initialised" do   before(:each) { Singleton.__init__(MySingleton) } end 
like image 65
Ranmocy Avatar answered Sep 19 '22 15:09

Ranmocy


Tough question, singletons are rough. In part for the reason that you're showing (how to reset it), and in part because they make assumptions that have a tendency to bite you later (e.g. most of Rails).

There are a couple of things you can do, they're all "okay" at best. The best solution is to find a way to get rid of singletons. This is hand-wavy, I know, because there isn't a formula or algorithm you can apply, and it removes a lot of convenience, but if you can do it, it's often worthwhile.

If you can't do it, at least try to inject the singleton rather than accessing it directly. Testing might be hard right now, but imagine having to deal with issues like this at runtime. For that, you'd need infrastructure built in to handle it.

Here are six approaches I have thought of.


Provide an instance of the class, but allow the class to be instantiated. This is the most in line with the way singletons are traditionally presented. Basically any time you want to refer to the singleton, you talk to the singleton instance, but you can test against other instances. There's a module in the stdlib to help with this, but it makes .new private, so if you want to use it you'd have to use something like let(:config) { Configuration.send :new } to test it.

class Configuration   def self.instance     @instance ||= new   end    attr_writer :credentials_file    def credentials_file     @credentials_file || raise("credentials file not set")   end end  describe Config do   let(:config) { Configuration.new }    specify '.instance always refers to the same instance' do     Configuration.instance.should be_a_kind_of Configuration     Configuration.instance.should equal Configuration.instance   end    describe 'credentials_file' do       specify 'it can be set/reset' do       config.credentials_file = 'abc'       config.credentials_file.should == 'abc'       config.credentials_file = 'def'       config.credentials_file.should == 'def'     end      specify 'raises an error if accessed before being initialized' do       expect { config.credentials_file }.to raise_error 'credentials file not set'     end   end end 

Then anywhere you want to access it, use Configuration.instance


Making the singleton an instance of some other class. Then you can test the other class in isolation, and don't need to test your singleton explicitly.

class Counter   attr_accessor :count    def initialize     @count = 0   end    def count!     @count += 1   end end  describe Counter do   let(:counter) { Counter.new }   it 'starts at zero' do     counter.count.should be_zero   end    it 'increments when counted' do     counter.count!     counter.count.should == 1   end end 

Then in your app somewhere:

MyCounter = Counter.new 

You can make sure to never edit the main class, then just subclass it for your tests:

class Configuration   class << self     attr_writer :credentials_file   end    def self.credentials_file     @credentials_file || raise("credentials file not set")   end end  describe Config do   let(:config) { Class.new Configuration }   describe 'credentials_file' do       specify 'it can be set/reset' do       config.credentials_file = 'abc'       config.credentials_file.should == 'abc'       config.credentials_file = 'def'       config.credentials_file.should == 'def'     end      specify 'raises an error if accessed before being initialized' do       expect { config.credentials_file }.to raise_error 'credentials file not set'     end   end end 

Then in your app somewhere:

MyConfig = Class.new Configuration 

Ensure that there is a way to reset the singleton. Or more generally, undo anything you do. (e.g. if you can register some object with the singleton, then you need to be able to unregister it, in Rails, for example, when you subclass Railtie, it records that in an array, but you can access the array and delete the item from it).

class Configuration   def self.reset     @credentials_file = nil   end    class << self     attr_writer :credentials_file   end    def self.credentials_file     @credentials_file || raise("credentials file not set")   end end  RSpec.configure do |config|   config.before { Configuration.reset } end  describe Config do   describe 'credentials_file' do       specify 'it can be set/reset' do       Configuration.credentials_file = 'abc'       Configuration.credentials_file.should == 'abc'       Configuration.credentials_file = 'def'       Configuration.credentials_file.should == 'def'     end      specify 'raises an error if accessed before being initialized' do       expect { Configuration.credentials_file }.to raise_error 'credentials file not set'     end   end end 

Clone the class instead of testing it directly. This came out of a gist I made, basically you edit the clone instead of the real class.

class Configuration     class << self     attr_writer :credentials_file   end    def self.credentials_file     @credentials_file || raise("credentials file not set")   end end  describe Config do   let(:configuration) { Configuration.clone }    describe 'credentials_file' do       specify 'it can be set/reset' do       configuration.credentials_file = 'abc'       configuration.credentials_file.should == 'abc'       configuration.credentials_file = 'def'       configuration.credentials_file.should == 'def'     end      specify 'raises an error if accessed before being initialized' do       expect { configuration.credentials_file }.to raise_error 'credentials file not set'     end   end end 

Develop the behaviour in modules, then extend that onto singleton. Here is a slightly more involved example. Probably you'd have to look into the self.included and self.extended methods if you needed to initialize some variables on the object.

module ConfigurationBehaviour   attr_writer :credentials_file   def credentials_file     @credentials_file || raise("credentials file not set")   end end  describe Config do   let(:configuration) { Class.new { extend ConfigurationBehaviour } }    describe 'credentials_file' do       specify 'it can be set/reset' do       configuration.credentials_file = 'abc'       configuration.credentials_file.should == 'abc'       configuration.credentials_file = 'def'       configuration.credentials_file.should == 'def'     end      specify 'raises an error if accessed before being initialized' do       expect { configuration.credentials_file }.to raise_error 'credentials file not set'     end   end end 

Then in your app somewhere:

class Configuration     extend ConfigurationBehaviour end 
like image 27
Joshua Cheek Avatar answered Sep 17 '22 15:09

Joshua Cheek