[ACCEPTED]-Resetting a singleton instance in Ruby-rspec2
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 1 even better add to before callback:
describe MySingleton, "#not_initialised" do
before(:each) { Singleton.__init__(MySingleton) }
end
Tough question, singletons are rough. In 46 part for the reason that you're showing 45 (how to reset it), and in part because they 44 make assumptions that have a tendency to 43 bite you later (e.g. most of Rails).
There 42 are a couple of things you can do, they're 41 all "okay" at best. The best solution 40 is to find a way to get rid of singletons. This 39 is hand-wavy, I know, because there isn't 38 a formula or algorithm you can apply, and 37 it removes a lot of convenience, but if 36 you can do it, it's often worthwhile.
If 35 you can't do it, at least try to inject 34 the singleton rather than accessing it directly. Testing 33 might be hard right now, but imagine having 32 to deal with issues like this at runtime. For 31 that, you'd need infrastructure built in 30 to handle it.
Here are six approaches I have 29 thought of.
Provide an instance of the class, but allow the class to be instantiated. This is the most in line with 28 the way singletons are traditionally presented. Basically 27 any time you want to refer to the singleton, you 26 talk to the singleton instance, but you 25 can test against other instances. There's 24 a module in the stdlib to help with this, but 23 it makes .new
private, so if you want to use 22 it you'd have to use something like let(:config) { Configuration.send :new }
to 21 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 20 it, use Configuration.instance
Making the singleton an instance of some other class. Then you 19 can test the other class in isolation, and 18 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 17 in your app somewhere:
MyCounter = Counter.new
You can make sure 16 to never edit the main class, then just 15 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 14 that there is a way to reset the singleton. Or more generally, undo 13 anything you do. (e.g. if you can register 12 some object with the singleton, then you 11 need to be able to unregister it, in Rails, for 10 example, when you subclass Railtie
, it records 9 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 8 of testing it directly. This came out of 7 a gist I made, basically you edit the clone 6 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 5 extend that onto singleton. Here is a slightly 4 more involved example. Probably you'd have 3 to look into the self.included
and self.extended
methods if you needed 2 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 1 in your app somewhere:
class Configuration
extend ConfigurationBehaviour
end
To extract a TL;DR from the nice longer 3 answer above, for future lazy visitors like 2 me - I found this to be clean and easy:
If 1 you had this before
let(:thing) { MyClass.instance }
Do this instead
let(:thing) { MyClass.clone.instance }
More Related questions
We use cookies to improve the performance of the site. By staying on our site, you agree to the terms of use of cookies.