在Ruby中重置单例示例

vojdkbi0  于 2023-04-20  发布在  Ruby
关注(0)|答案(3)|浏览(220)

如何在Ruby中重置一个单例对象?我知道在真实的的代码中永远不会有人想这样做,但是单元测试呢?
下面是我在RSpec测试中尝试做的事情-

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

它失败了,因为我之前的一个测试初始化了Singleton对象。我试过遵循Ian白色在this链接中的建议,该链接本质上是猴子补丁Singleton提供reset_instance方法,但我得到了一个未定义的方法'reset_instance'异常。

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中实现这一点最惯用的方法是什么?

quhf5bfb

quhf5bfb1#

一个坚韧的问题是,单例是粗糙的,部分原因是你正在展示(如何重置它),部分原因是他们做出的假设有可能在以后咬你(例如大多数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时,它会将其记录在数组中,但你可以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

克隆类而不是直接测试。这是我做的一个要点,基本上你编辑克隆而不是真实的的类。

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

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

wz3gfoph2#

我想简单地这样做就可以解决你的问题:

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
yhqotfr8

yhqotfr83#

如果你以前有这个

let(:thing) { MyClass.instance }

不如这样做

let(:thing) { MyClass.clone.instance }
  • 摘录自较长的公认答案。*

相关问题