21

如何在 Ruby 中重置单例对象?我知道一个人永远不想在真正的代码中这样做,但是单元测试呢?

这是我在 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”异常。

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 中最惯用的方法是什么?

4

3 回答 3

30

我想只需这样做就可以解决您的问题:

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
于 2013-02-24T14:40:48.760 回答
28

棘手的问题,单身人士很粗糙。部分原因是您正在展示(如何重置它),部分原因是他们做出的假设倾向于稍后咬您(例如大多数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.includedand方法。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
于 2012-08-26T08:11:38.133 回答
3

从上面更长的答案中提取一个 TL;DR,对于像我这样的未来懒惰的访客 - 我发现这很干净和容易:

如果你以前有这个

let(:thing) { MyClass.instance }

改为这样做

let(:thing) { MyClass.clone.instance }
于 2016-05-21T15:25:59.083 回答