在Ruby中重置单例实例

如何在Ruby中重置单个对象? 我知道在实际代码中我们永远不想这样做但是unit testing呢?

这是我在RSpec测试中尝试做的事情 –

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

它失败了,因为我之前的一个测试初始化​​了单例对象。 我试过从这个链接开始关注Ian White的建议,它基本上是猴子补丁Singleton来提供reset_instance方法,但我得到一个未定义的方法’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 

在Ruby中最常用的方法是什么?

棘手的问题,单身人士很粗暴。 部分原因在于您正在展示(如何重置它),部分原因是它们做出的假设有时候会让您感到困惑(例如大多数Rails)。

你可以做几件事,他们最好都“好”。 最好的解决方案是找到摆脱单身人士的方法。 这是手工波浪,我知道,因为你没有可以应用的公式或算法,它消除了很多便利,但如果你能做到,它通常是值得的。

如果你做不到,至少尝试注入单例而不是直接访问它。 测试现在可能很难,但想象一下在运行时必须处理这样的问题。 为此,您需要内置的基础架构来处理它。

这是我想到的六种方法。


提供类的实例,但允许实例化类 。 这与单身传统的呈现方式最为一致。 基本上,只要您想要引用单例,就可以与单例实例进行对话,但是您可以针对其他实例进行测试。 stdlib中有一个模块可以帮助解决这个问题,但是它会使.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 

在模块中开发行为 ,然后将其扩展到单例。 这是一个稍微复杂的例子。 如果你需要在对象上初始化一些变量,你可能需要查看self.includedself.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 

我想这样做可以解决你的问题:

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

或者甚至更好地在回调之前添加:

 describe MySingleton, "#not_initialised" do before(:each) { Singleton.__init__(MySingleton) } end 

提取一个TL; DR来自上面那个更好的更长的答案,对于像我这样的未来懒惰的访客 – 我发现这很简单:

如果你之前有这个

 let(:thing) { MyClass.instance } 

这样做

 let(:thing) { MyClass.clone.instance }