棘手的问题,单身人士很粗糙。部分原因是您正在展示(如何重置它),部分原因是他们做出的假设倾向于稍后咬您(例如大多数Rails)。
您可以做几件事,充其量它们都是“好的”。最好的解决方案是找到摆脱单例的方法。我知道,这是手动操作的,因为没有可以应用的公式或算法,并且它消除了很多便利,但如果你能做到,它通常是值得的。
如果你做不到,至少尝试注入单例而不是直接访问它。现在测试可能很难,但想象一下在运行时必须处理这样的问题。为此,您需要内置的基础设施来处理它。
这是我想到的六种方法。
提供类的实例,但允许类被实例化。这是最符合单例的传统呈现方式。基本上任何时候你想引用单例,你都会与单例实例交谈,但你可以针对其他实例进行测试。标准库中有一个模块可以帮助解决这个问题,但它是.new
私有的,所以如果你想使用它,你必须使用类似的东西let(:config) { Configuration.send :new }
来测试它。
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
然后在任何你想访问它的地方,使用Configuration.instance
使单例成为其他类的实例。然后您可以单独测试其他类,而无需显式测试您的单例。
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
然后在您的应用程序某处:
MyCounter = Counter.new
您可以确保永远不要编辑主类,然后将其子类化以进行测试:
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
然后在您的应用程序某处:
MyConfig = Class.new Configuration
确保有办法重置单例。或者更一般地说,撤消您所做的任何事情。(例如,如果您可以使用单例注册某个对象,那么您需要能够取消注册它,例如,在 Rails 中,当您创建子类时Railtie
,它会将其记录在一个数组中,但您可以访问该数组并从中删除该项目它)。
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
克隆类而不是直接测试它。这是我提出的一个要点,基本上你编辑克隆而不是真正的类。
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
在 modules 中开发行为,然后将其扩展到单例。这是一个稍微复杂的例子。如果您需要初始化对象上的一些变量,您可能必须查看self.included
and方法。self.extended
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
然后在您的应用程序某处:
class Configuration
extend ConfigurationBehaviour
end