Resetting a singleton instance in Ruby
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) endend
or even better add to before callback:
describe MySingleton, "#not_initialised" do before(:each) { Singleton.__init__(MySingleton) }end
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") endenddescribe 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 endend
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 endenddescribe 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 endend
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") endenddescribe 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 endend
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") endendRSpec.configure do |config| config.before { Configuration.reset }enddescribe 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 endend
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") endenddescribe 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 endend
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") endenddescribe 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 endend
Then in your app somewhere:
class Configuration extend ConfigurationBehaviourend